WPF MVVM UserControls的最佳实践。避免绑定问题

时间:2014-02-14 06:01:38

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

我正在努力成为一名优秀的士兵并设计一些简单的用户控件,以便在WPF MVVM应用程序中使用。我正在尝试(尽可能)使UserControls自己使用MVVM,但我不认为调用应用程序应该知道。调用应用程序应该只能打下用户控件,可能设置一个或两个属性,也许订阅事件。就像他们使用常规控件(ComboBox,TextBox等)时一样,我有一点时间让绑定正确。注意在下面的View中使用ElementName。这不是使用DataContext。不用多说,这是我的控制:

<UserControl x:Class="ControlsLibrary.RecordingListControl"
         ...
         x:Name="parent"

         d:DesignHeight="300" d:DesignWidth="300">
<Grid >
    <StackPanel Name="LayoutRoot">
    <ListBox ItemsSource="{Binding ElementName=parent,Path=Recordings}" Height="100" Margin="5" >
        <ListBox.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Path=FullDirectoryName}" />
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
    </StackPanel>

</Grid>

在后面的代码中(如果有办法避免代码落后,请告诉我)...

public partial class RecordingListControl : UserControl
{
    private RecordingListViewModel vm = new RecordingListViewModel();
    public RecordingListControl()
    {
        InitializeComponent();

        // I have tried the next two lines at various times....
       // LayoutRoot.DataContext = vm;
        //DataContext = vm;
    }


    public static FrameworkPropertyMetadata md = new FrameworkPropertyMetadata(new PropertyChangedCallback(OnPatientId));
    // Dependency property for PatientId
    public static readonly DependencyProperty PatientIdProperty =
   DependencyProperty.Register("PatientId", typeof(string), typeof(RecordingListControl), md);

    public string PatientId
    {
        get { return (string)GetValue(PatientIdProperty); }
        set { SetValue(PatientIdProperty, value);
        //vm.SetPatientId(value);
        }
    }

    // this appear to allow us to see if the dependency property is called.
    private static void OnPatientId(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        RecordingListControl ctrl = (RecordingListControl)d;
        string temp = ctrl.PatientId;     
    }

在我的ViewModel中,我有:

public class RecordingListViewModel : ViewModelBase
{
    private ObservableCollection<RecordingInfo> _recordings = null;// = new ObservableCollection<string>();
    public RecordingListViewModel()
    {

    }
    public ObservableCollection<RecordingInfo> Recordings
    {
        get
        {
            return _recordings;
        }
    }

    public void SetPatientId(string patientId)
    {
        // bunch of stuff to fill in _recordings....
        OnPropertyChanged("Recordings");
    }

}

然后我把这个控件放在我的主窗口中,就像这样:

<Grid>
    <ctrlLib:RecordingListControl  PatientId="{Binding PatientIdMain}" SessionId="{Binding SessionIdMain}" />
    <Label Content="{Binding PatientIdMain}" /> // just to show binding is working for non-controls
</Grid>

我运行所有这些时得到的错误是:

System.Windows.Data Error: 40 : BindingExpression path error: 'Recordings' property not found on 'object' ''RecordingListControl' (Name='parent')'. BindingExpression:Path=Recordings; DataItem='RecordingListControl' (Name='parent'); target element is 'ListBox' (Name=''); target property is 'ItemsSource' (type 'IEnumerable')

显然我有某种绑定问题。这实际上比我得到的要远得多。至少我正在使用后面的控件代码中的代码: OnPatientId。

之前,我没有在User Control中使用ElementName并且正在使用DataContext并且收到一个绑定错误,表明PatientIdMain被视为用户控件的成员。

有人能指出我在MVVM应用程序中使用MVVM设计的用户控件的示例吗?我认为这是一种相当普遍的模式。

如果我能提供更多详细信息,请告诉我。

非常感谢, 戴夫

编辑1 我尝试了har07的想法(参见其中一个答案)。我有: 如果我尝试:

ItemsSource="{Binding ElementName=parent,Path=DataContext.Recordings}"

我得到了

System.Windows.Data Error: 40 : BindingExpression path error: 'Recordings' property not found on 'object' ''MainViewModel' (HashCode=59109011)'. BindingExpression:Path=DataContext.Recordings; DataItem='RecordingListControl' (Name='parent'); target element is 'ListBox' (Name=''); target property is 'ItemsSource' (type 'IEnumerable')

如果我尝试:

ItemsSource="{Binding Recordings}"

我得到了

System.Windows.Data Error: 40 : BindingExpression path error: 'Recordings' property not found on 'object' ''MainViewModel' (HashCode=59109011)'. BindingExpression:Path=Recordings; DataItem='MainViewModel' (HashCode=59109011); target element is 'ListBox' (Name=''); target property is 'ItemsSource' (type 'IEnumerable')

我认为他的第一个想法(也许是他的第二个想法)非常接近,但回想一下,录像是在ViewModel中定义的,而不是视图。不知何故,我需要告诉XAML使用viewModel作为源。这就是设置DataContext所做的事情,但正如我在主要部分中所说,这会在其他地方产生问题(您会遇到与从MainWindown绑定到控件上的属性相关的绑定错误)。

编辑2.如果我尝试har07的第一个建议:

ItemsSource="{Binding ElementName=parent,Path=DataContext.Recordings}"

并添加控件的代码:

RecordingListViewModel vm = new RecordingListViewModel();
DataContext = vm;

我明白了:

System.Windows.Data Error: 40 : BindingExpression path error: 'PatientIdMain' property not found on 'object' ''RecordingListViewModel' (HashCode=33515363)'. BindingExpression:Path=PatientIdMain; DataItem='RecordingListViewModel' (HashCode=33515363); target element is 'RecordingListControl' (Name='parent'); target property is 'PatientId' (type 'String')
换句话说,控件看起来很好,但依赖项属性与主窗口的绑定似乎搞得一团糟。编译器假定PatientIdMain是RecordingListViewModel的一部分 各种帖子表明,由于这个原因,我无法设置DataContext。它会破坏绑定到主窗口。参见例如: Binding to a dependency property of a user control WPF/XAML并查看Marc的回答。

3 个答案:

答案 0 :(得分:3)

您不应在UserControl中设置x:Name,因为该控件只有一个Name属性,通常会通过使用控件的代码设置。因此,您无法使用ElementName绑定来绑定UserControl本身的属性。在其内容中绑定UserControl属性的另一种方法是使用

{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type my:RecordingsControl}}, Path=Recordings}

以同样的方式,使用该控件的客户端代码可以显式地或通过继承来设置其DataContext。因此,只要显示控件,就会丢弃在构造函数中创建的vm实例,并替换为继承的DataContext。

你可以做两件事来解决这个问题:要么在UserControl之外创建vm,例如作为主窗口的ViewModel的属性,并像这样使用UserControl:

<my:RecordingsControl DataContext="{Binding RecordingListVM}"/>

这样,您就不需要任何代码,上面的Binding只会改为

{Binding Recordings}

或者,在UserControl的代码隐藏文件中创建一个Recordings属性,并按照我在第一个代码示例中所示绑定到它。

答案 1 :(得分:1)

如果绑定语句以这种方式改变,你会得到什么:

ItemsSource="{Binding ElementName=parent,Path=DataContext.Recordings}"

或者这样:

ItemsSource="{Binding Recordings}"

如果以上绑定方式之一解决当前绑定错误(“ BindingExpression路径错误:'找不到'录制'属性... ”),但导致另一个绑定错误请发布后一个错误消息。

我认为这部分的正确绑定声明如上所述。

更新:

回复您的修改。尝试在StackPanel级别本地设置DataContext,这样就可以将UserControl设置为不同的DataContext:

public RecordingListControl()
{
    InitializeComponent();
    RecordingListViewModel vm = new RecordingListViewModel();
    LayoutRoot.DataContext = vm;
}

我再次看到你已经尝试了这个,但我认为这是解决特定绑定错误的正确方法(“ BindingExpression路径错误:'PatientIdMain'属性未找到...... ”),所以,让我知道这是否解决了错误,但导致另一个绑定错误。

答案 2 :(得分:1)

一个可能的答案是(灵感来自Dependency property binding usercontrol with a viewmodel

只需将DataContext用于UserControl,不要使用任何ElementName业务 即。

 public partial class RecordingListControl : UserControl
{
    public RecordingListViewModel vm = new RecordingListViewModel();
    public RecordingListControl()
    {
        InitializeComponent();
        **DataContext = vm;**
    }
 ....

然后,在MainWindow中,您需要像这样绑定用户控件:

<ctrlLib:RecordingListControl  
     Name="_ctrlRecordings" 
     PatientId="{Binding  RelativeSource={RelativeSource 
AncestorType=Window},Path=DataContext.PatientIdMain, Mode=TwoWay}"
     SessionId="{Binding  RelativeSource={RelativeSource 
AncestorType=Window},Path=DataContext.SessionIdMain, Mode=TwoWay}"   />

我不会将此标记为答案,因为(除了回答自己的问题的大胆)我不太喜欢这个答案。它迫使应用程序员记住把所有关于AncestorType和RelativeSource的废话都放进去。我不认为必须用标准控件来做这个,为什么在这里呢?