如何正确绑定到 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>
这是带有DependencyProperty
的InkStringView
用户控件。
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这是您永远不应该直接从UserControl
本身设置DataContext
的众多原因之一。
当你这样做时,你不能再使用任何其他 DataContext
,因为 UserControl 的 DataContext
被硬编码为只有 UserControl
可以访问的实例,这违背了 WPF 的最大优势之一拥有独立的 UI 和数据层。
在 WPF 中使用UserControls
主要有两种方式
一个独立的UserControl
可以在任何地方使用,而不需要特定的DataContext
。
这种类型的UserControl
通常会为它需要的任何值公开DependencyProperties
,并且会像这样使用:
<v:InkStringView TextInControl="Binding SomeValue" />
我能想到的典型示例是任何通用的东西,例如 Calendar 控件或 Popup 控件。
UserControl
仅用于特定的 Model
或 ViewModel
。
这些UserControls
对我来说更为常见,并且可能是您正在寻找的。我将如何使用这样的 UserControl 的示例如下:
<v:InkStringView DataContext="Binding MyInkStringViewModelProperty" />
或者更频繁地,它将与隐式DataTemplate
一起使用。隐式 DataTemplate
是带有 DataType
而没有 Key
的 DataTemplate
,并且 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.ItemTemplate
或ItemsControl.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 屏幕截图)。我认为只编写<ContentPresenter>
标签比编写<ItemsControl>
标签并解释它如何在 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<string>
)。
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
这听起来不存在。它可能与<v:InkStringView TextInControl="Binding DataContext.text, RelativeSource=RelativeSource AncestorType=x:Type ContentPresenter" />
一起使用,然后它会找到包装该项目的<ContentPresenter>
项目并绑定到DataContext.text
,它应该是Strings[x].text
,但我不确定以上是关于如何正确绑定到 MVVM 框架中用户控件的依赖属性的主要内容,如果未能解决你的问题,请参考以下文章