【问题标题】:Best practices for WPF MVVM UserControls. Avoiding Binding problemsWPF MVVM 用户控件的最佳实践。避免绑定问题
【发布时间】:2017-11-07 14:05:24
【问题描述】:

我正在努力成为一名优秀的士兵并设计一些用于 WPF MVVM 应用程序的简单用户控件。我正在尝试(尽可能)让 UserControls 自己使用 MVVM,但我认为调用应用程序不应该知道这一点。调用应用程序应该能够轻击用户控件,也许设置一两个属性,也许订阅事件。就像他们使用常规控件(ComboBox、TextBox 等)时一样,我需要花很长时间才能正确绑定。注意下面视图中 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。

之前,我在用户控件中没有 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')

我认为他的第一个想法(也许是他的第二个)非常接近,但回想一下,Recordings 是在 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 的答案。

【问题讨论】:

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


    【解决方案1】:

    您不应该在 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 属性,并按照我在第一个代码示例中显示的那样绑定到它。

    【讨论】:

    • 嗯。不知道我能不能按照你的想法。我不认为主窗口应该知道任何关于用户控件的 viewModel 的信息,如果这是你的建议的话。你能给出(或指出)一个更详细的例子吗?
    • @Dave hbarck 说您不能在控件本身上设置 Name 或 DataContext (您已经完成或尝试过这两者),因为它们会与(即替换或被替换,具体取决于它们的设置位置)控件的消费者合理地期望设置或继承他或她自己的值。克服Name 和DataContext 问题的建议分别是好的和不太好的(出于您建议的原因)。要克服 DC 问题,您应该将其设置在 StackPanel "LayoutRoot" 而不是控件上;外部的Grid 可以去掉。
    【解决方案2】:

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

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

    或者这样:

    ItemsSource="{Binding Recordings}"
    

    如果上述绑定方式之一解决了当前绑定错误(“BindingExpression path error: 'Recordings' property not found...”),但导致另一个绑定错误,请发布后一个错误消息.

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

    更新:

    响应您的编辑。尝试在 StackPanel 级别本地设置 DataContext,这样您就可以将 UserControl 设置为不同的 DataContext :

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

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

    【讨论】:

    • 感谢您的及时回复。我尝试了您的建议并将结果报告为对我上面主要帖子的编辑。你的想法似乎很接近,非常接近......非常感谢,戴夫
    • 更新了解决最近绑定错误的建议。
    • 谢谢 har07!那解决了它。我以为我以前尝试过,但显然我有一个错误。标记为答案和感谢。这是一个巨大的帮助。
    • 不客气。顺便说一句,好问题,我觉得这很清楚(所有且仅发布相关信息)
    【解决方案3】:

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

    对 UserControl 只使用 DataContext,不要使用任何 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 的废话。我认为不必使用标准控件来执行此操作,那么为什么在这里呢?

    【讨论】:

    • 来自未来的问候。我和你有同样的问题,我同意你的观点,那就是它制造了无稽之谈。我已经尝试了我在网上找到的所有其他“解决方案”,但我都不喜欢它们。它们都涉及为我的视图模型丢弃 MVVM 或不得不改变我进行绑定的方式。我已经放弃了 DP,现在只需在后面的代码中添加一行代码即可建立主从链接
    猜你喜欢
    • 2012-02-19
    • 2011-04-07
    • 2010-12-15
    • 2015-12-14
    • 2010-11-28
    • 2018-11-03
    • 1970-01-01
    • 1970-01-01
    • 2023-03-05
    相关资源
    最近更新 更多