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

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

我正在尝试:

  1. 从调用 ItemsControl 设置DependencyProperty,然后
  2. 使 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。我建议阅读这个主题。
  • @Clemens 嗨。从调用 ItemsControl 中删除 ElementName= ... 仍然会显示“我是第 1 行”,并且第 0 行上的文本块应该是空白行。 ???想法?
  • 字符串是 ObservableCollection 吗?
  • @Lee O 是的。 Strings 是 ViewModel 中的 ObservableCollection,用于初始调用 ItemsControl。顺便说一句,如果我用一个简单的 TextBlock 替换用户控件,则所有内容都会显示并使用上述绑定正确运行。

标签: wpf mvvm data-binding user-controls dependency-properties


【解决方案1】:

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

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

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

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

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

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

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

  2. 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,但我不确定
    猜你喜欢
    • 2018-12-19
    • 1970-01-01
    • 1970-01-01
    • 2012-03-29
    • 2015-02-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多