了解数据绑定如何在UserControl的DependencyProperty及其主机应用程序的属性

时间:2016-12-05 19:11:36

标签: c# wpf mvvm data-binding mvvm-light

我有一个非常简单的要求,其中UserControl需要为用户提供从其下拉列表中选择项目的方法。当用户单击按钮时,UserControl将执行一些内部测试,然后它将调用主机应用程序中的方法并将其传递给用户的选择。

我使用MVVM进行此操作,但我对使其工作所必须做的事感到有点困惑。根据我对数据绑定的经验,我的知识似乎仍然存在一些差距,因为每个新实现似乎都让我遇到了一些我没想过的问题。

我的UserControl很简单,它是一个下拉列表和一个按钮:

<UserControl x:Class="MyUserControl.UserControl1"
             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" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <StackPanel Orientation="Vertical">
        <ComboBox ItemsSource="{Binding MyItems}" IsSynchronizedWithCurrentItem="True" SelectedItem="{Binding MySelectedItem}" />
        <Button Content="Click me" Command="{Binding ClickMeCommand}" />
    </StackPanel>
</UserControl>

背后的代码如下所示,只是设置控件的数据:

using GalaSoft.MvvmLight.CommandWpf;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace MyUserControl
{
    /// <summary>
    /// Interaction logic for UserControl1.xaml
    /// </summary>
    public partial class UserControl1 : UserControl
    {
        public ICollectionView MyItems { get; set; }
        public RelayCommand ClickMeCommand { get; set; }
        public string MySelectedItem { get; set; }

        public ICommand HostClickMeCommand
        {
            get { return (ICommand)GetValue(HostClickMeCommandProperty); }
            set { SetValue(HostClickMeCommandProperty, value); }
        }

        // Using a DependencyProperty as the backing store for HostClickMeCommand.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty HostClickMeCommandProperty =
            DependencyProperty.Register("HostClickMeCommand", typeof(ICommand), typeof(UserControl1), new PropertyMetadata(null));

        public UserControl1()
        {
            InitializeComponent();
            DataContext = this;
            MyItems = CollectionViewSource.GetDefaultView( new List<string> { "John", "Mary", "Joe" });
            ClickMeCommand = new RelayCommand( ExecuteClickMeCommand);
        }

        private void ExecuteClickMeCommand()
        {
            MessageBox.Show( "Hello from user control!");
            if( HostClickMeCommand != null) {
                HostClickMeCommand.Execute( MySelectedItem);
            }
        }
    }
}

您会注意到我的UserControl的按钮点击处理程序将显示一条消息,然后调用我的应用程序。

应用程序的XAML也非常简单:

<Window x:Class="MyHostApplication.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:uc="clr-namespace:MyUserControl;assembly=MyUserControl"
        Title="MainWindow" Height="350" Width="525">
    <DockPanel>
        <uc:UserControl1 HostClickMeCommand="{Binding MyHostClickMeCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />
    </DockPanel>
</Window>

它的代码隐藏:

using GalaSoft.MvvmLight.CommandWpf;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace MyHostApplication {
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public RelayCommand<string> MyHostClickMeCommand { get; set; }

        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;
            MyHostClickMeCommand = new RelayCommand<string>( (name) => { MessageBox.Show( String.Format( "Hello from host, {0}!", name)); });
        }
    }
}

此代码工作正常。

但我的问题是:为什么我必须在绑定中指定RelativeSource?由于应用程序窗口的DataContext本身,为什么Window会将UserControl的依赖属性绑定到MyHostClickMeCommand?如果我删除了RelativeSource,则不会调用应用程序的处理程序。

我还应该补充一点,我想找出定义绑定的正确方法的原因是因为我希望能够将我的应用程序的ViewModel设置为另一个类。理想情况下,我希望我的应用程序能够在XAML中使用它:

<Window.DataContext>
    <local:MainViewModel />
</Window.DataContext>

其中MainViewModel位于我的应用程序项目文件的ViewModels文件夹中。

2 个答案:

答案 0 :(得分:1)

每当我在主应用程序启动之外看到这个:

DataContext = this;

或者

<UserControl.DataContext>
    <local:MyViewModel />
</UserControl.DataContext>

它总是为我抛出红旗。

除非这是一个特殊情况,否则我强烈建议永远对UserControl中的DataContext属性进行硬编码。通过这样做,您可以阻止任何其他DataContext传递给UserControl,这会破坏WPF具有单独的UI和数据层的最大优势之一。

专门构建UserControl以用于特定模型或ViewModel用作DataContext,例如:

<!-- Draw anything of type MyViewModel with control MyUserControl-->
<!-- DataContext will automatically set to the MyViewModel -->
<DataTemplate DataType="{x:Type local:MyViewModel}}">
    <local:MyUserControl /> 
</DataTemplate>

或者构建它,期望DataContext可以绝对是任何东西,并且DependencyProperites将用于为控件提供所需的数据:

<!-- DataContext property can be anything, as long as it has a property called MyDataProperty -->
<local:MyUserControl MyDependencyProperty="{Binding MyDataProperty}" />

但是要回答你的问题,你需要在绑定中使用RelativeSource的原因是因为绑定默认使用DataContext,所以它试图绑定到UserControl1.DataContext.MyHostClickMeCommand。由于您在构造函数中有硬编码DataContext = this;,因此它尝试绑定到不存在的MyUserControl1.MyHostClickMeCommandRelativeSource的使用告诉绑定它应该从当前`DataContext以外的东西获取它。

我看到很多关于WPF初学者的DataContext的混淆,我通常将它们发送到t his StackOverflow Answer about the DataContext is for

答案 1 :(得分:1)

创建类似以下

的绑定时
<TextBox x:Name="foo" Text="{Binding MuhText}" />

这相当于以下

foo.Text = foo.DataContext.MuhText;

绑定路径以绑定控件的DataContext为根。当你这么说时,

<UserControl x:Class="MyUserControl.UserControl1"
    RemoveUselessNoise="true" >
    <StackPanel Orientation="Vertical">
        <ComboBox ItemsSource="{Binding MyItems}" 
                  SelectedItem="{Binding MySelectedItem}" />
        <Button Content="Click me" Command="{Binding ClickMeCommand}" />
    </StackPanel>
</UserControl>

你仍然绑定了控件的DataContext(ComboBox和Button)。

您希望绑定到定义了这些控件的UserControl实例。有些人建议你做这样的事情:

public UserControl1()
{
    InitializeComponent();
    DataContext = this;
}

这些人是疯狂的黑客,他们的存在只是为了让你在路上遇到失败。这通常会导致绑定无法按预期工作,并且通常会中断DataContext的流程。

你正在做几乎所有事情。您的解决方案是删除DataContext=this;然后将绑定重新绑定到UserControl。您可以通过多种方式执行此操作,但我认为最简单的方法是为您的根提供x:Name并使用ElementName绑定。

<UserControl x:Class="MyUserControl.UserControl1"
             x:Name="RootNodeLol"
             RemoveUselessNoise="true" >
    <StackPanel Orientation="Vertical">
        <ComboBox ItemsSource="{Binding MyItems, ElementName=RootNodeLol}" 
                  SelectedItem="{Binding MySelectedItem, ElementName=RootNodeLol}" />
        <Button Content="Click me" Command="{Binding ClickMeCommand,
                                                      ElementName=RootNodeLol}" />
    </StackPanel>
</UserControl>

假设这里没有其他的手淫,你应该好好接受。

旁注,你应该抓一个Snoop的副本。您可以在运行时检查绑定,并查看事情未按预期工作的原因。