如何正确绑定到 MVVM 框架中用户控件的依赖属性

Posted

技术标签:

【中文标题】如何正确绑定到 MVVM 框架中用户控件的依赖属性【英文标题】:How to correctly bind to a dependency property of a usercontrol in a MVVM framework 【发布时间】:2014-10-29 14:09:57 【问题描述】:

我一直找不到一个干净、简单的示例来说明如何正确在 MVVM 框架内使用具有 DependencyProperty 的 WPF 实现用户控件。每当我为用户控件分配DataContext 时,下面的代码都会失败。

我正在尝试:

    从调用 ItemsControl 设置DependencyProperty,然后 使 DependencyProperty 的值可用于被调用用户控件的 ViewModel。

我还有很多东西要学,真诚感谢任何帮助。

这是最顶层用户控件中的ItemsControl,它使用DependencyProperty TextInControl 调用InkStringView 用户控件(来自另一个问题的示例)。

<ItemsControl ItemsSource="Binding Strings" x:Name="self" >

    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel HorizontalAlignment="Left" VerticalAlignment="Top" Orientation="Vertical" />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>


    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <DataTemplate.Resources>
                <Style TargetType="v:InkStringView">
                    <Setter Property="FontSize" Value="25"/>
                    <Setter Property="HorizontalAlignment" Value="Left"/>
                </Style>
            </DataTemplate.Resources>

            <v:InkStringView TextInControl="Binding text, ElementName=self"  />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

这是带有DependencyPropertyInkStringView 用户控件。

XAML:

<UserControl x:Class="Nova5.UI.Views.Ink.InkStringView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         x:Name="mainInkStringView"
         mc:Ignorable="d" 
         d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>   
            <RowDefinition/>
        </Grid.RowDefinitions>

        <TextBlock Grid.Row="0" Text="Binding TextInControl, ElementName=mainInkStringView" />
        <TextBlock Grid.Row="1" Text="I am row 1" />
    </Grid>
</UserControl>

代码隐藏文件:

namespace Nova5.UI.Views.Ink

    public partial class InkStringView : UserControl
    
        public InkStringView()
        
            InitializeComponent();
            this.DataContext = new InkStringViewModel();   <--THIS PREVENTS CORRECT BINDING, WHAT
                                                           --ELSE TO DO?????

        public String TextInControl
        
            get  return (String)GetValue(TextInControlProperty); 
            set  SetValue(TextInControlProperty, value); 
        

        public static readonly DependencyProperty TextInControlProperty =
            DependencyProperty.Register("TextInControl", typeof(String), typeof(InkStringView));

    

【问题讨论】:

ItemsControl(或任何派生类)的 ItemTemplate 中控件的 DataContext 由 WPF 自动分配给源集合中的相应项。在您的情况下,这将是 String 集合中的一个元素。如果Strings 是具有text 属性的对象的集合,您只需将DataTemplate 中的绑定写为TextInControl="Binding text" 并且不显式设置 任何其他DataContext。我建议阅读这个主题。 见Data Binding Overview和Data Templating Overview。 @Clemens 嗨。从调用 ItemsControl 中删除 ElementName= ... 仍然会显示“我是第 1 行”,并且第 0 行上的文本块应该是空白行。 ???想法? 字符串是 ObservableCollection 吗? @Lee O 是的。 Strings 是 ViewModel 中的 ObservableCollection,用于初始调用 ItemsControl。顺便说一句,如果我用一个简单的 TextBlock 替换用户控件,则所有内容都会显示并使用上述绑定正确运行。 【参考方案1】:

这是您永远不应该直接从UserControl 本身设置DataContext 的众多原因之一。

当你这样做时,你不能再使用任何其他 DataContext,因为 UserControl 的 DataContext 被硬编码为只有 UserControl 可以访问的实例,这违背了 WPF 的最大优势之一拥有独立的 UI 和数据层。

在 WPF 中使用UserControls 主要有两种方式

    一个独立的UserControl 可以在任何地方使用,而不需要特定的DataContext

    这种类型的UserControl 通常会为它需要的任何值公开DependencyProperties,并且会像这样使用:

    <v:InkStringView TextInControl="Binding SomeValue" />
    

    我能想到的典型示例是任何通​​用的东西,例如 Calendar 控件或 Popup 控件。

    UserControl 仅用于特定的 ModelViewModel

    这些UserControls 对我来说更为常见,并且可能是您正在寻找的。我将如何使用这样的 UserControl 的示例如下:

    <v:InkStringView DataContext="Binding MyInkStringViewModelProperty" />
    

    或者更频繁地,它将与隐式DataTemplate 一起使用。隐式 DataTemplate 是带有 DataType 而没有 KeyDataTemplate,并且 WPF 将在任何时候想要呈现指定类型的对象时自动使用此模板。

    <Window.Resources>
        <DataTemplate DataType="x:Type m:InkStringViewModel">
            <v:InkStringView />
        </DataTemplate>
    <Window.Resources>
    
    <!-- Binding to a single ViewModel -->
    <ContentPresenter Content="Binding MyInkStringViewModelProperty" />
    
    <!-- Binding to a collection of ViewModels -->
    <ItemsControl ItemsSource="Binding MyCollectionOfInkStringViewModels" />
    

    使用此方法时不需要ContentPresenter.ItemTemplateItemsControl.ItemTemplate

不要把这两种方法混在一起,效果不好:)


但无论如何,更详细地解释您的具体问题

当您像这样创建用户控件时

<v:InkStringView TextInControl="Binding text"  />

你基本上是在说

var vw = new InkStringView()
vw.TextInControl = vw.DataContext.text;

vw.DataContext 未在 XAML 中的任何位置指定,因此它从父项继承,从而导致

vw.DataContext = Strings[x];

所以设置TextInControl = vw.DataContext.text 的绑定是有效的,并且在运行时解析得很好。

但是,当您在 UserControl 构造函数中运行它时

this.DataContext = new InkStringViewModel();

DataContext 设置为一个值,因此不再自动从父级继承。

所以现在运行的代码如下所示:

var vw = new InkStringView()
vw.DataContext = new InkStringViewModel();
vw.TextInControl = vw.DataContext.text;

当然,InkStringViewModel 没有名为 text 的属性,因此绑定在运行时失败。

【讨论】:

有什么好的资源可以学习你上面解释的两种方法吗? @EhsanSajjad 我现在想不出任何概念,但是一旦你意识到它是如何工作的,它们就会很容易记住。 A) 以这样的方式编写您的 UserControl XAML,无论DataContext 是什么(通常通过使用 DependencyProperties 完成,就像 OP 一样),或者 B) 假设DataContext 是特定数据类型(通常是模型或视图模型),编写您的UserControl XAML。在任何时候,您都不应该在 UserControl 内部设置 DataContext 属性 :) @Rachel So...在情况2中,UserControl的ViewModel实际上与调用模块的ViewModel相同??我对 ContentPresenter 不熟悉...它适合哪里? @AlanWayne 是的,情况 #2 中的 ViewModel 将来自创建 UserControl 的 DataContext。在 OP 的情况下,它可以是 Strings[x] 的任何值。 @Alan 而ContentPresenter 是用于在 UI 中显示任何对象的对象,甚至是非 UI 对象,例如 ViewModel。它实际上被ItemsControl 用来渲染ItemsSource 集合中的任何内容(有关ItemsControl 如何渲染的示例,请参见my blog post about ItemsControl 顶部的Snoop 屏幕截图)。我认为只编写&lt;ContentPresenter&gt; 标签比编写&lt;ItemsControl&gt; 标签并解释它如何在 UI 中呈现对象更简单:)【参考方案2】:

似乎您正在将父视图的模型与 UC 的模型混合。

这是一个与您的代码匹配的示例:

MainViewModel:

using System.Collections.Generic;

namespace UCItemsControl

    public class MyString
    
        public string text  get; set; 
    

    public class MainViewModel
    
        public ObservableCollection<MyString> Strings  get; set; 

        public MainViewModel()
        
            Strings = new ObservableCollection<MyString>
            
                new MyString text = "First" ,
                new MyString text = "Second" ,
                new MyString text = "Third" 
            ;
        
    

使用它的主窗口:

<Window x:Class="UCItemsControl.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:v="clr-namespace:UCItemsControl"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <v:MainViewModel></v:MainViewModel>
    </Window.DataContext>
    <Grid>
        <ItemsControl 
                ItemsSource="Binding Strings" x:Name="self" >

            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <StackPanel HorizontalAlignment="Left" VerticalAlignment="Top" Orientation="Vertical" />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>


            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <DataTemplate.Resources>
                        <Style TargetType="v:InkStringView">
                            <Setter Property="FontSize" Value="25"/>
                            <Setter Property="HorizontalAlignment" Value="Left"/>
                        </Style>
                    </DataTemplate.Resources>

                    <v:InkStringView TextInControl="Binding text"  />
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</Window>

你的 UC(没有设置 DataContext):

public partial class InkStringView : UserControl

    public InkStringView()
    
        InitializeComponent();
    

    public String TextInControl
    
        get  return (String)GetValue(TextInControlProperty); 
        set  SetValue(TextInControlProperty, value); 
    

    public static readonly DependencyProperty TextInControlProperty =
        DependencyProperty.Register("TextInControl", typeof(String), typeof(InkStringView));

(你的 XAML 没问题)

这样我可以得到我猜是预期的结果,一个值列表:

First
I am row 1
Second
I am row 1
Third
I am row 1

【讨论】:

【参考方案3】:

你快到了。问题是您正在为您的 UserControl 创建一个 ViewModel。这是一种气味。

从外部看,用户控件的外观和行为应该与任何其他控件一样。您正确地在控件上公开了属性,并将内部控件绑定到这些属性。都对。

您失败的地方是尝试为所有内容创建 ViewModel。所以放弃那个愚蠢的 InkStringViewModel,让使用该控件的人将他们的视图模型绑定到它。

如果您想问“视图模型中的逻辑呢?如果我摆脱它,我将不得不将代码放入代码隐藏中!”我回答:“它是业务逻辑吗?无论如何都不应该嵌入到您的 UserControl 中。而且 MVVM != 没有代码隐藏。为您的 UI 逻辑使用代码隐藏。它属于它。”

【讨论】:

逻辑上每个组件都有一个模型,有时它被拆分为 n 个属性(FirstName、LastName、Address...),有时你有一个复合对象(Person),有时是一个具有异构的整个模型数据。 @Pragmateek 有时牛会跳过月球,但这也与问题或这个答案无关。 我之所以添加此评论,是因为这是您回答的第一句话(“这是一种气味”),这并不总是正确的,只是想弄清楚。并且请不要被我不是投反对票的巧合所愚弄(我从不投反对票,因为我的个人资料会向您展示,尤其是没有像您一样享有盛誉的人):) @Pragmateek 嗯,我已经有足够长的时间了,不用担心否决票 :) 但是,当您发现自己创建一个专门供用户控件使用的视图模型时,我会认为它总是一种气味。它只是无法正常工作。 @AlanWayne 如果是 UI 代码,就像我说的那样,把它放在代码隐藏中。您需要在用户控制之外重构业务逻辑,这并不意味着创建一个新的 VM。也许您的用户控件包含过多的 UI?也许它应该被分解,以便您的业务逻辑可以很好地适合使用用户控件的页面/窗口的 VM?如果我有您的代码,很难确切地告诉您如何完成,但一个合理的指导是说“我的 UI 需要什么?添加一个属性,以便 VM 绑定可以将它提供给我。”【参考方案4】:

你需要在这里做两件事(我假设字符串是ObservableCollection&lt;string&gt;)。

1) 从 InkStringView 构造函数中删除 this.DataContext = new InkStringViewModel();。 DataContext 将是 Strings ObservableCollection 的一个元素。

2) 改变

<v:InkStringView TextInControl="Binding text, ElementName=self"  />

<v:InkStringView TextInControl="Binding " />

您拥有的 xaml 正在 ItemsControl 上寻找“文本”属性以将值 TextInControl 绑定到。我使用 DataContext(恰好是一个字符串)将 TextInControl 绑定到的 xaml。如果 Strings 实际上是一个带有 SomeProperty 字符串属性的 ObservableCollection,您希望将其绑定到该属性,然后将其更改为 this。

<v:InkStringView TextInControl="Binding SomeProperty" />

【讨论】:

假设是错误的。 Strings 是具有字符串类型的公共 text 属性的对象集合。 是的...所以使用底部选项 我使用他的代码对它进行了两种测试,并按照您在 cmets 中的假设修复了它。 TextInControl 的绑定仍然很好。 @Lee O 从代码隐藏中删除 DataContext = ... 设置为文本时会正确显示“SomeProperty”。但是,我不再有用户控件的数据上下文。 @LeeO。 #2 会将其从绑定到ItemsControl.text 更改为String[x].text,但是如果DataContext 仍在UserControl 构造函数中设置,我仍然认为这不会起作用,因为它正在尝试绑定到InkStringViewModel.text这听起来不存在。它可能与&lt;v:InkStringView TextInControl="Binding DataContext.text, RelativeSource=RelativeSource AncestorType=x:Type ContentPresenter" /&gt; 一起使用,然后它会找到包装该项目的&lt;ContentPresenter&gt; 项目并绑定到DataContext.text,它应该是Strings[x].text,但我不确定

以上是关于如何正确绑定到 MVVM 框架中用户控件的依赖属性的主要内容,如果未能解决你的问题,请参考以下文章

遵循MVVM模式,如何创建“设置”功能,在其他用户控件中设置数据绑定项值?

wpf MVVM框架基础

WPF MVVM 网易云音乐

如何让我的 WPF 用户控件的依赖属性更新我的视图模型?

MVVM - 当绑定属性不存在时隐藏控件

vue.js基础知识篇:简介数据绑定指令计算属性表单控件绑定和过滤器