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

14

我一直没有找到一个干净、简单的示例,说明如何在MVVM框架中使用WPF实现带有DependencyProperty的用户控件。我的代码在我将用户控件赋予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
请参阅数据绑定概述数据模板概述 - Clemens
@Clemens 你好。从调用ItemsControl中删除ElementName= ...仍然会导致显示"I am row 1",并且在应该有文本块的第0行上出现空白行。??? 有什么想法吗? - Alan Wayne
@Lee O 是的。在ViewModel中,Strings是ItemsControl最初调用的ObservableCollection。另外,如果我用一个简单的TextBlock替换usercontrol,以上绑定都能正确显示和运行。 - Alan Wayne
@Clemens 把用户控件改为使用RelativeSource后,仍然导致打印出一行空白。ObservableCollection是一个带有以下公共属性的类: { public string text { get { return _text; } set { if (_text != value) { _text = value; OnPropertyChanged("text"); } } } } - Alan Wayne
显示剩余4条评论
4个回答

17
这就是你不应该直接从UserControl本身设置DataContext的原因之一。这样做会导致你无法再使用其他DataContext,因为UserControl的DataContext被硬编码为仅UserControl才能访问的实例,这有点违反WPF将UI和数据层分开的最大优势之一。
在WPF中使用UserControls有两种主要方法:
  1. 一个独立的UserControl可以在任何地方使用而不需要特定的DataContext

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

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

我能想到的典型例子是一些通用的控件,比如日历控件或弹出框控件。

  • 一个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在运行时是有效的并且可以正常解析。

    然而,当您在用户控件构造函数中运行此代码时

    this.DataContext = new InkStringViewModel();
    

    DataContext已经设置了一个值,因此不再自动从其父级继承。

    现在运行的代码看起来像这样:

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

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


    1
    有没有好的资源可以学习你上面提到的两种方法? - Ehsan Sajjad
    1
    @EhsanSajjad 我现在想不出来任何例子,但一旦你意识到它的工作原理,这些概念就非常容易记住了。要么 A) 以这样的方式编写您的UserControl XAML,使其不会因为 DataContext 是什么而破坏(通常使用DependencyProperties,就像OP所做的那样),要么 B) 假设 DataContext 是特定数据类型(通常是Model或ViewModel),并编写您的UserControl XAML。在任何时候都不应该从UserControl本身设置 DataContext 属性 :) - Rachel
    @Rachel 所以...在第二种情况下,UserControl 的 ViewModel 实际上是调用模块的相同 ViewModel 吗?我不熟悉 ContentPresenter...它在哪里适用? - Alan Wayne
    @Alan,“ContentPresenter”是一个用于在UI中显示任何对象的对象,甚至可以是非UI对象,例如ViewModel。它实际上是由“ItemsControl”用于呈现“ItemsSource”集合中包含的任何内容的(有关“ItemsControl”如何呈现的示例,请参见我的有关ItemsControl的博客文章顶部的Snoop截图)。我认为只写“<ContentPresenter>”标记比编写“<ItemsControl>”标记并解释它如何在UI中呈现对象更简单 :) - Rachel
    @Rachel 一个隐式的DataTemplate能够与父DataType一起使用吗?也就是说,DataType是否可以被多个ViewModel继承,以便用户控件可以被多个ViewModel使用?我之所以问这个问题,是因为在这种情况下,我试图通过用户控件将语音识别作为输入添加到我的业务类中。但我很容易看出手写、键盘或运动也可以被使用。我希望将输入问题放入不同的用户控件中,并允许每个业务ViewModel使用每个不同的用户控件。这该怎么做呢? - Alan Wayne
    显示剩余2条评论

    5
    你就快成功了。问题在于你为UserControl创建了一个ViewModel,这是一种不好的做法。
    UserControls应该像其他控件一样,在外部看起来相同,并且具有相同的行为。你已经在控件上公开了属性,并将内部控件绑定到这些属性,这是正确的。
    你失败的地方在于试图为所有内容创建ViewModel。所以放弃那个愚蠢的InkStringViewModel吧,让使用该控件的人将他们的视图模型绑定到它上面。
    如果你想问“视图模型中的逻辑怎么办?如果我把它去掉,我得把代码放到CodeBehind里面!”我的回答是,“它是业务逻辑吗?这不应该嵌入到您的UserControl中。而且MVVM != 没有CodeBehind。使用CodeBehind处理UI逻辑,这是它属于的位置。”

    从逻辑上讲,每个组件都有一个模型,有时它被分成n个属性(名字,姓氏,地址...),有时您只有一个复合对象(人),有时是整个包含异构数据的模型。 - Pragmateek
    2
    @Pragmateek,有时候牛会跳过月亮,但这与问题或答案毫无关系。 - user1228
    我添加这个评论是因为你的回答中第一个评论(“这是一种气味”)并不总是正确的,我只是想澄清一下。请不要被巧合所迷惑,我不是那个给你点踩的人(我的个人资料会告诉你我从不点踩,尤其不会点踩像你这样有很高声望的人) :) - Pragmateek
    @Will 谢谢。你说得大多数都是对的,我的疑问是如果我既没有 code-behind 也没有 viewmodel,我该如何管理与 usercontrol 相关的代码逻辑。不过,在这种情况下,这个逻辑并非业务逻辑,而是实现额外功能的逻辑,这些额外功能并非业务代码逻辑的一部分。 - Alan Wayne
    2
    @Pragmateek 哎,我已经历经多年,不用担心被点踩的事情 :) 然而,当你发现自己为了某个用户控件而创建一个专门的视图模型时,我会考虑这是否存在问题。这样做通常是行不通的。 - user1228
    2
    @AlanWayne 如果是UI代码,就像我说的那样将其放入codebehind中。你需要将业务逻辑从用户控件中重构出来,这并不意味着创建一个新的VM。也许你的用户控件包含了太多的UI?也许它应该被拆分,以便你的业务逻辑可以很好地适应在页面/窗口中使用的用户控件的VM中?如果我有你的代码,可能很难告诉你如何做到这一点,但一个合理的指导原则是:“我的UI需要什么?添加一个属性,以便VM绑定可以给我。” - user1228

    3

    看起来您混淆了父视图的模型和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
    

    2

    在这里,您需要完成两件事情(我假设Strings是一个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上的“Text”属性,以将TextInControl的值绑定到该属性。我使用DataContext放置的XAML将TextInControl绑定到字符串。如果Strings实际上是一个具有要绑定的字符串属性SomeProperty的ObservableCollection,则应改为以下内容。
    <v:InkStringView TextInControl="{Binding SomeProperty}" />
    

    假设是错误的。Strings 是一个对象集合,具有类型为字符串的公共 text 属性。 - Clemens
    是的...所以请使用底部选项。 - Lee O.
    我使用他的代码进行了双向测试,这样修复问题就像你在评论中所假设的那样。但是,TextInControl的绑定仍然很好。 - Lee O.
    @Lee O 从代码后台中删除 DataContext = ... 将正确显示设置为文本的“SomeProperty”。但是,我不再拥有用户控件的数据上下文。 - Alan Wayne
    @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,但我不确定。 - Rachel
    @Rachel 抱歉如果我表达不清,但我的意思是他们需要同时完成1和2才能使其正常工作。 - Lee O.

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接