【问题标题】:Bind a VM to different views, depending on VM's properties根据 VM 的属性将 VM 绑定到不同的视图
【发布时间】:2017-07-17 04:59:09
【问题描述】:

现在我有以下代码:

视图模型

public class VMBase
{
   public string TabID{get;set;}
   public string TabHeader {get;set;}
}

public class VM1:VMBase //implements the properties in base class
{
}

public class VM2:VMBase //implements the properties in base class
{
}

在我的DataTemplate.xaml 中,我将不同的本地控件绑定到ViewModel,具体取决于ViewModel 的类型,即:

<DataTemplate DataType="{x:Type VM:VM1}">
  <local: Control1 />
</DataTemplate>
<DataTemplate DataType="{x:Type VM:VM2}">
  <local: Control2 />
</DataTemplate>

Control1Control2UserControl 的不同类型:

public class Control1:UserControl
{
}

public class Control2:UserControl
{
}

当我只有两个VMBase 的派生类时,事情仍然可以管理,但是如果我有十个呢?或者更多?会变丑的。

是否可以将单个 VM 绑定到不同的视图(用户控件),这样我就不必为VMBase 手动创建这么多派生类?我只需要指定TabIDTabHeader 等VM 属性,结果将绑定正确的视图。

编辑:

这里有更多详细信息:我的 VM 绑定到 ContentControl(即:contentcontrol.Content=VM)。每个虚拟机都有两个属性TabIDHeader。是否应该调用 DataTemplateSelector 取决于它是否具有特定的 TabID (如果它有其他 TabID 则不应调用此 DataTemplateSelector ),以及调用哪个 DataTemplate ( DataTemplateSelector 内部的逻辑)进一步取决于在Header。如何实现?

【问题讨论】:

  • 使用 Caliburn micro,这是完全自动和透明的。它在某处使用 ViewLocator 方法。考虑使用 MVVM 框架或至少收集他们的代码。
  • @HenkHolterman,也许您想展示一些有关 Caliburn micro 如何解决此问题的示例?
  • 不清楚您要解决什么问题。你的问题太模糊了。如果只显示TabIDTabHeader,为什么每个模型子类都需要不同的模板?如果每个模型子类都有您想要显示的附加属性,为什么不为基类定义一个模板,然后将它(通过ContentPresenter)合并到您的子类模板中?请提供足够的上下文,以便您的问题明确无误,包括良好的minimal reproducible example 和对代码“丑陋”的准确解释。
  • @PeterDuniho,我只是想重构上面的代码,这样我就不必定义几十个VMBase的子类了。我需要为每个模型子类使用不同的模板,因为视图控件不同
  • " 因为视图控件不同" -- 以什么方式?请回答我实际提出的问题。

标签: c# wpf xaml mvvm


【解决方案1】:

更新答案 - v2(根据问题编辑)

我认为当TabID 不匹配时,只需在DataTemplateSelector 中返回null 就可以了,因为WPF 将尝试选择下一个最佳匹配项(即与DataType 匹配的模板)。如果TabID 匹配,您可以根据TabHeader 值返回模板。

所以你的自定义 DataTemplateSelector 看起来像这样:

public class TabHeaderDataTemplateSelector : DataTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        var element = container as FrameworkElement;
        if (element == null)
            return null;

        var viewModel = item as VMBase;
        if (viewModel == null || viewModel.TabID != "02") 
            return null; //continue only if TabID is a match

        if (viewModel != null)
        {
            switch(viewModel.TabHeader)
            {
                case "two":
                    return element.FindResource($"Template2") as DataTemplate;
                case "three":
                    return element.FindResource($"Template3") as DataTemplate;
            }
        }

        return null;
    }
}

XAML 示例

<Window.Resources>
    <!-- data template for VM1 -->
    <DataTemplate DataType="{x:Type local:VM1}">
        <Grid>
            <Rectangle Stroke="Black" />
            <TextBlock Margin="5" Text="{Binding TabHeader}" FontSize="18"/>
        </Grid>
    </DataTemplate>

    <!-- data template for VM2 -->
    <DataTemplate DataType="{x:Type local:VM2}">
        <Grid>
            <Rectangle Stroke="Red" />
            <TextBlock Margin="5" Text="{Binding TabHeader}" FontSize="18"/>
        </Grid>
    </DataTemplate>

    <DataTemplate x:Key="Template2">
        <Grid>
            <Ellipse Stroke="Green" StrokeThickness="4"/>
            <TextBlock Margin="10" Text="{Binding TabHeader}" FontSize="24" 
                Foreground="Red" FontWeight="Bold"
                HorizontalAlignment="Center" VerticalAlignment="Center" />
        </Grid>
    </DataTemplate>

    <DataTemplate x:Key="Template3">
        <Grid>
            <TextBlock Margin="10" Text="{Binding TabHeader}" FontSize="24" 
                Foreground="White" Background="Black" FontWeight="Bold"
                HorizontalAlignment="Center" VerticalAlignment="Center" />
        </Grid>
    </DataTemplate>

    <Style TargetType="ContentControl">
        <Setter Property="ContentTemplateSelector">
            <Setter.Value>
                <local:TabHeaderDataTemplateSelector />
            </Setter.Value>
        </Setter>
    </Style>
</Window.Resources>
<StackPanel Margin="25">
    <ContentControl Content="{Binding VmObj_1}" />
    <ContentControl Content="{Binding VmObj_2}" />
    <ContentControl Content="{Binding VmObj_3}" />
    <ContentControl Content="{Binding VmObj_4}" />
</StackPanel>

和代码隐藏

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        this.DataContext = new
        {
            VmObj_1 = new VM1 { TabID = "01", TabHeader = "one" },
            VmObj_2 = new VM1 { TabID = "02", TabHeader = "two" },
            VmObj_3 = new VM2 { TabID = "02", TabHeader = "three" },
            VmObj_4 = new VM2 { TabID = "03", TabHeader = "four" },
        };
    }
}

public class VMBase
{
    public string TabID { get; set; }
    public string TabHeader { get; set; }
}

public class VM1 : VMBase { }

public class VM2 : VMBase { }

更新答案 - v1

您可以通过两种不同的方式解决此问题。每个选项都有自己的优缺点;但我最推荐的方法是(正如@jon-stødle 建议的那样)是使用DataTemplateSelector

选项 1 - 使用 DataTemplateSelector

当您使用基于 Type 的数据模板时,我假设您很可能使用 ContentControl(或变体)来显示动态视图模型驱动的 UI。 ContentControl 和其他模板化控件,如 LabelUserControlItemsControlListBox 等通常具有依赖属性,如 ContentTemplateSelectorItemTemplateSelector,您可以将模板选择器绑定到。

您可以参考this link 以获取使用LabelDataTemplateSelector 的示例;或以下示例用于TabControl

XAML:

<Window.Resources>
    <DataTemplate x:Key="Tab1Template">
        <Grid>
            <Rectangle Stroke="Black" />
            <TextBlock Margin="5" Text="{Binding}" FontSize="18"/>
        </Grid>
    </DataTemplate>

    <DataTemplate x:Key="Tab2Template">
        <Grid>
            <Ellipse Stroke="Green" StrokeThickness="4"/>
            <TextBlock Margin="10" Text="{Binding}" FontSize="24" 
                        Foreground="Red" FontWeight="Bold"
                        HorizontalAlignment="Center" VerticalAlignment="Center" />
        </Grid>
    </DataTemplate>

    <DataTemplate x:Key="Tab3Template">
        <Grid>
            <TextBlock Margin="10" Text="{Binding}" FontSize="24" 
                        Foreground="White" Background="Black" FontWeight="Bold"
                        HorizontalAlignment="Center" VerticalAlignment="Center" />
        </Grid>
    </DataTemplate>

    <Style TargetType="{x:Type TabControl}">
        <Setter Property="ContentTemplateSelector">
            <Setter.Value>
                <local:TabIdDataTemplateSelector />
            </Setter.Value>
        </Setter>
    </Style>
</Window.Resources>
<TabControl>
    <TabControl.ItemsSource>
        <col:ArrayList>
            <sys:String>1</sys:String>
            <sys:String>2</sys:String>
            <sys:String>3</sys:String>
        </col:ArrayList>
    </TabControl.ItemsSource>
</TabControl>

数据模板选择器:

public class TabIdDataTemplateSelector : DataTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        var element = container as FrameworkElement;
        if (element == null)
            return null;

        //var vm = item as VMBase;
        //var id = vm.TabId;
        string id = item as string;
        if (id != null)
        {
            return element.FindResource($"Tab{id}Template") as DataTemplate;
        }

        return null;
    }

}

选项 2 - 使用基于 Style 的数据触发器

另一种选择是在 ViewModel 的属性(即 TabId)上使用数据触发器来更新容器视图的 ContentTemplate(即 TabControl)。

<Window.Resources>
    <DataTemplate x:Key="Tab1Template">
        <Grid>
            <Rectangle Stroke="Black" />
            <TextBlock Margin="5" Text="{Binding Key}" FontSize="18"/>
        </Grid>
    </DataTemplate>

    <DataTemplate x:Key="Tab2Template">
        <Grid>
            <Ellipse Stroke="Green" StrokeThickness="4"/>
            <TextBlock Margin="10" Text="{Binding Key}" FontSize="24" 
                    Foreground="Red" FontWeight="Bold"
                    HorizontalAlignment="Center" VerticalAlignment="Center" />
        </Grid>
    </DataTemplate>

    <DataTemplate x:Key="Tab3Template">
        <Grid>
            <TextBlock Margin="10" Text="{Binding Key}" FontSize="24" 
                    Foreground="White" Background="Black" FontWeight="Bold"
                    HorizontalAlignment="Center" VerticalAlignment="Center" />
        </Grid>
    </DataTemplate>

    <Style TargetType="{x:Type TabControl}">
        <Style.Triggers>
            <DataTrigger Binding="{Binding TabId}" Value="1">
                <Setter Property="ContentTemplate" Value="{StaticResource ResourceKey=Tab1Template}" />
            </DataTrigger>
            <DataTrigger Binding="{Binding TabId}" Value="2">
                <Setter Property="ContentTemplate" Value="{StaticResource ResourceKey=Tab2Template}" />
            </DataTrigger>
            <DataTrigger Binding="{Binding TabId}" Value="3">
                <Setter Property="ContentTemplate" Value="{StaticResource ResourceKey=Tab3Template}" />
            </DataTrigger>
        </Style.Triggers>
    </Style>
</Window.Resources>
<TabControl DisplayMemberPath="Value" SelectedValuePath="Key" 
            SelectedValue="{Binding TabId}">
    <TabControl.ItemsSource>
        <col:Hashtable>
            <sys:String x:Key="1">one</sys:String>
            <sys:String x:Key="2">two</sys:String>
            <sys:String x:Key="3">three</sys:String>
        </col:Hashtable>
    </TabControl.ItemsSource>
</TabControl>

【讨论】:

  • 您的解决方案似乎不错,但是否可以将 Style 绑定到 ViewModel(在我的情况下,它是 VMBase),而不是 View 元素(在这种情况下,是TabControl)?
  • 只是为了更好地理解您的问题 - 我们在这里讨论的是哪个选项?因为在选项 3 中,样式仅适用于 DataContext 属性。
  • style targettype 的目标是 tabcontrol,它是一个视图,对吗?但是我所有的差异化属性,例如 tabid,都与 VM 相关,而不是每秒的 tabcontrol
  • 更新了答案并提供了更多说明 - 删除了样式触发选项,因为它可能不适用于您的情况。
  • 抱歉没有让我的问题更清楚:我的 VM 绑定到 ContentControl(即:contentcontrol.Content=VM)。每个虚拟机都有两个属性TabIDHeader。是否应该调用DataTemplateSelector 取决于它是否具有特定的TabID,以及调用哪个DataTemplate 进一步取决于Header。如何实现?
【解决方案2】:

您正在寻找的是DataTemplateSelector。它让您可以根据不同的标准选择DataTemplate

public class TaskListDataTemplateSelector : DataTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        var element = container as FrameworkElement;
        var vmBase = item as VMBase;

        if (element != null && vmBase != null)
        {
            switch(vmBase.TabID)
            {
                case "Tab1": return element.FindResource("Tab1Template") as DataTemplate;
                case "Tab2": return element.FindResource("Tab2Template") as DataTemplate;
                default: return null;
            }
        }
    }
}

您可以在docs 中阅读更多关于它们的信息,或查看this 教程。

【讨论】:

  • 检查后,我不太确定它对我的问题有何帮助:我可以将 TaskListDataTemplateSelector 绑定到哪些对象?这里没有任何 ListBox 等价物
  • 假设您使用的是 WPF,ListView 应该可以完成这项工作。它还具有ItemTemplateSelector 属性
猜你喜欢
  • 2011-05-20
  • 2012-03-08
  • 1970-01-01
  • 1970-01-01
  • 2022-01-15
  • 2023-03-22
  • 1970-01-01
  • 2022-01-05
  • 1970-01-01
相关资源
最近更新 更多