【问题标题】:How to have a dynamic DataTemplateSelector如何拥有动态 DataTemplateSelector
【发布时间】:2017-07-03 23:33:02
【问题描述】:

我有一个在 Xamarin Forms ListView 中显示的可观察集合。我已经定义了一个详细信息和一个摘要模板,用于查看每个列表项。我希望能够根据每个项目中的布尔属性在摘要和详细信息模板之间动态切换。

这是项目。

public class MyItem : INotifyPropertyChanged
{
    bool _switch = false;
    public bool Switch
    {
        get
        {
            return _switch;
        }
        set
        {
            if (_switch != value)
            {
                _switch = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Switch"));
            }
        }

    }
    public int Addend1 { get; set; }
    public int Addend2 { get; set; }
    public int Result
    {
        get
        {
            return Addend1 + Addend2;
        }
    }
    public string Summary
    {
        get
        {
            return Addend1 + " + " + Addend2 + " = " + Result;
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
}

这是可观察的集合。请注意,每当开关值更改时,我都会删除该项目并重新插入。这样做的原因是强制 ListView 重新选择 DataTemplate。

public class MyItems : ObservableCollection<MyItem>
{
    protected override void InsertItem(int index, MyItem item)
    {
        item.PropertyChanged += MyItems_PropertyChanged;
        base.InsertItem(index, item);
    }
    protected override void RemoveItem(int index)
    {
        this[index].PropertyChanged -= MyItems_PropertyChanged;
        base.RemoveItem(index);
    }
    private void MyItems_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        int index = IndexOf(sender as MyItem);
        if(index >= 0)
        {
            RemoveAt(index);
            Insert(index, sender as MyItem);
        }
    }
}

这是我的数据模板选择器...

public class MyItemTemplateSelector : DataTemplateSelector
{
    DataTemplate Detail { get; set; }
    DataTemplate Summary { get; set; }

    protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
    {
        if(item is MyItem)
        {
            return (item as MyItem).Switch ? Detail : Summary;
        }
        return null;
    }
}

这是我的资源定义...

        <DataTemplate x:Key="MyDetail">
            <ViewCell>
                <StackLayout Orientation="Horizontal">
                    <Switch IsToggled="{Binding Switch}"/>
                    <Entry Text="{Binding Addend1}"/>
                    <Entry Text="{Binding Addend2}"/>
                    <Label Text="{Binding Result}"/>
                </StackLayout>
            </ViewCell>
        </DataTemplate>
        <DataTemplate x:Key="MySummary">
            <ViewCell>
                <StackLayout Orientation="Horizontal">
                    <Switch IsToggled="{Binding Switch}"/>
                    <Label Text="{Binding Summary}" VerticalOptions="Center"/>
                </StackLayout>
            </ViewCell>
        </DataTemplate>
        <local:MyItemTemplateSelector x:Key="MySelector" Detail="{StaticResource MyDetail}" Summary="{StaticResource MySummary}"/>

这是我的集合初始化...

        MyItems = new MyItems();
        MyItems.Add(new MyItem() { Switch = true, Addend1 = 1, Addend2 = 2 });
        MyItems.Add(new MyItem() { Switch = false, Addend1 = 1, Addend2 = 2 });
        MyItems.Add(new MyItem() { Switch = true, Addend1 = 2, Addend2 = 3 });
        MyItems.Add(new MyItem() { Switch = false, Addend1 = 2, Addend2 = 3 });

这就是它的样子……

没错。所以一切正常。如果切换开关,项目的视图将从摘要更改为详细信息。问题是这不可能是正确的方法!删除列表项并将其放回同一位置以重新选择数据模板是一个完整的方法。但我想不出另一种方法。在 WPF 中,我使用项容器样式中的数据触发器来根据开关值设置内容模板,但在 Xamarin 中似乎没有办法做等效的事情。

【问题讨论】:

    标签: c# listview xamarin xamarin.forms


    【解决方案1】:

    做到这一点的方法不是通过切换模板,而是将内容视图定义为模板并更改模板内控件的可见性。显然没有办法让 ListView 重新评估项目上的项目模板,除非删除它并重新添加它。

    这是我的内容视图...

    <ContentView xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamarinFormsBench"
             x:Class="XamarinFormsBench.SummaryDetailView">
    <ContentView.Content>
      <StackLayout x:Name="stackLayout" Orientation="Horizontal">
            <Switch x:Name="toggle" IsToggled="{Binding Switch}"/>
            <Entry x:Name="addend1" Text="{Binding Addend1}"/>
            <Entry x:Name="addend2" Text="{Binding Addend2}"/>
            <Label x:Name="result" Text="{Binding Result}"/>
            <Label x:Name="summary" Text="{Binding Summary}" VerticalOptions="Center"/>
        </StackLayout>
    </ContentView.Content>
    

    这是后面的代码...

    namespace XamarinFormsBench
    {
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class SummaryDetailView : ContentView
    {
        public SummaryDetailView()
        {
            InitializeComponent();
            toggle.PropertyChanged += Toggle_PropertyChanged;
            UpdateVisibility();
        }
    
        private void Toggle_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            if(e.PropertyName == "IsToggled")
            {
                UpdateVisibility();
            }
        }
    
        private void UpdateVisibility()
        {
            bool isDetail = toggle.IsToggled;
            addend1.IsVisible = isDetail;
            addend2.IsVisible = isDetail;
            result.IsVisible = isDetail;
            summary.IsVisible = !isDetail;
            InvalidateLayout();  // this is key!
        }
    }
    }
    

    现在主页包含这个...

        <ListView ItemsSource="{Binding MyItems}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <local:SummaryDetailView/>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    

    使这项工作正常工作的关键是在摘要和详细信息之间切换时使 ContentView 的布局无效。这会强制 ListView 再次布局单元格。没有这个,使不可见的控件消失,使可见的控件永远不会显示。如果 ContentView 在 ListView 之外使用,则不需要这个。在我看来,这似乎是 ListView 中的一个错误。如果可以使 ViewCell 的布局无效,则可以使项目模板切换工作,但没有公共方法(只有受保护的方法)可以做到这一点。

    【讨论】:

      【解决方案2】:

      几年前这对我来说是个棘手的问题。我来到了 MarkupExtensions 和转换器 (IValueConverter)。经过与 XAML 扩展领域的激烈斗争,我发现了一件显而易见的事情:不应该那样做。

      对于组件的(m)任何属性的动态更改,您应该使用样式。属性的反应(它必须是 DependencyProperty 才能使用组件)更改很容易通过 Stryle.Triggers 和 Setter 设置。

      <Style x:Key="imbXmlTreeView_itemstyle" TargetType="TreeViewItem">
          <Setter Property="Margin" Value="-23,0,0,0" />
          <Setter Property="Padding" Value="1" />
          <Setter Property="Panel.Margin" Value="0"/>
          <Style.Triggers>
              <Trigger Property="IsSelected" Value="True">
                  <Setter Property="Background" Value="{DynamicResource fade_lightGray}" />
                  <Setter Property="Foreground"  Value="{DynamicResource fade_darkGray}" />
              </Trigger>
              <Trigger Property="IsSelected" Value="False">
                  <Setter Property="Background" Value="{DynamicResource fade_lightGray}" />
                  <Setter Property="Foreground" Value="{DynamicResource fade_darkGray}" />
              </Trigger>
          </Style.Triggers>
      </Style>
      

      考虑上面(只是从我的旧项目中复制的):DynamicResource 可以是您的 DataTemplate。

      这是您可能使用的更准确的示例:

      <Style x:Key="executionFlowBorder" TargetType="ContentControl" >
              <Setter Property="Margin" Value="5" />
              <Setter Property="ContentTemplate" >
                  <Setter.Value>
                      <DataTemplate>
                          <StackPanel Orientation="Vertical">
                          <Border Style="{DynamicResource executionBorder}" DataContext="{Binding}">
                              <Grid>
                                  <Grid.ColumnDefinitions>
                                      <ColumnDefinition Width="20" />
                                      <ColumnDefinition Width="1*"/>
                                      <ColumnDefinition Width="20" />
                                  </Grid.ColumnDefinitions>
                                  <CheckBox IsChecked="{Binding Path=isExecuting}" Content="" Grid.Column="0" VerticalAlignment="Center"/>
                                  <Label Content="{Binding Path=displayName, Mode=OneWay}" FontSize="10" Grid.Column="1" FontStretch="Expanded"  FontWeight="Black"/>
                                  <Image Source="{Binding Path=iconSource, Mode=OneWay}" Width="16" Height="16" Grid.Column="2" HorizontalAlignment="Right" Margin="0,0,5,0"/>
                              </Grid>
                          </Border>
                          <Label Content="{Binding Path=displayComment, Mode=OneWay}" FontSize="9" HorizontalAlignment="Left"/>
                          </StackPanel>
                      </DataTemplate>
                  </Setter.Value>
              </Setter>
          </Style>
      

      setter 的值可以是 DynamicResource 或通过您的 MarkupExtension 传递的值 - 就像我在这里所做的那样:

      using System; 
          using System.Windows;
          using System.Windows.Markup;
      
          #endregion
      
          /// <summary>
          /// Pristup glavnom registru resursa
          /// </summary>
          [MarkupExtensionReturnType(typeof (ResourceDictionary))]
          public class masterResourceExtension : MarkupExtension
          {
              public masterResourceExtension()
              {
              }
      
              public override object ProvideValue(IServiceProvider serviceProvider)
              {
                  try
                  {
                      return imbXamlResourceManager.current.masterResourceDictionary;
                  }
                  catch
                  {
                      return null;
                  }
              }
          }
      

      您正在使用的 MarkupExtensions 如下例所示: 在 XAML 代码中:

      <Image Grid.Row="1" Name="image_splash" Source="{imb:imbImageSource ImageName=splash}" Stretch="Fill" />
      

      稍后添加: 只是不要忘记在 XAML 窗口/控件顶部添加命名空间/程序集引用(指向具有自定义 MarkupExtension 的代码)(在此示例中为 imbCore.xaml 来自同一解决方案的不同库项目):

      <Window x:Class="imbAPI.imbDialogs.imbSplash"
              xmlns:imb="clr-namespace:imbCore.xaml;assembly=imbCore"
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              Title="{Binding Path=splashTitle}" Height="666" Width="896" ResizeMode="NoResize" WindowStyle="ToolWindow" Topmost="False" WindowStartupLocation="CenterScreen"
              xmlns:imbControls="clr-namespace:imbAPI.imbControls">
          <Grid>
      

      还请记住,您必须先对其进行编译才能使其在 XAML 设计器中工作。


      所用扩展的C#代码:

       using System; 
          using System.Windows.Markup;
          using System.Windows.Media;
          using imbCore.resources;
      
          #endregion
      
          [MarkupExtensionReturnType(typeof (ImageSource))]
          public class imbImageSourceExtension : MarkupExtension
          {
              public imbImageSourceExtension()
              {
              }
      
              public imbImageSourceExtension(String imageName)
              {
                  this.ImageName = imageName;
              }
      
              [ConstructorArgument("imageName")]
              public String ImageName { get; set; }
      
              public override object ProvideValue(IServiceProvider serviceProvider)
              {
                  try
                  {
      
                      if (imbCoreApplicationSettings.doDisableIconWorks) return null;
                      return imbIconWorks.getIconSource(ImageName);
      
                  }
                  catch
                  {
                      return null;
                  }
              }
          }
      

      希望我一开始就回答了你的问题 :)。 现在我得睡觉了:)。祝你好运!

      稍后添加:好的,我错过了您的观点 :) 抱歉。但是,如果您在我发布的代码中发现有用的东西,我会留下回复。再见!

      【讨论】:

      • 我认为这一切都适用于 WPF 而不是 Xamarin Forms。我对 Xamarin Forms 的发现是,只有 WPF 中最基本的东西才有效。
      猜你喜欢
      • 2020-06-20
      • 1970-01-01
      • 1970-01-01
      • 2023-04-01
      • 2012-02-07
      • 2018-12-23
      • 1970-01-01
      • 2021-12-26
      • 1970-01-01
      相关资源
      最近更新 更多