如何拥有动态DataTemplateSelector

时间:2017-07-03 23:33:03

标签: c# listview xamarin xamarin.forms

我有一个可观察的集合,我在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 });

这就是它的样子......

enter image description here

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

2 个答案:

答案 0 :(得分: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的布局无效,您可以让项目模板切换工作,但是没有公共方法(只有受保护的方法)来执行此操作。

答案 1 :(得分:0)

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

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

<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;
            }
        }
    }

希望我在第一时间得到你的问题:)。 现在我要小睡:)。祝你好运!

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