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

4
我正在尝试成为一名优秀的士兵,并设计一些简单的用户控件,供WPF MVVM应用程序使用。我正在尽可能地让用户控件本身使用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设计的用户控件示例吗?我认为这是一个相当常见的模式。
如果需要更多细节,请告诉我。
非常感谢, Dave 编辑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;

I get:

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个回答

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 Recordings}

或者,在用户控件的代码后台文件中创建一个Recordings属性,并像我在第一个代码示例中展示的那样进行绑定。


嗯,我不确定我能够理解你的想法。如果你是在建议这个,我不认为主窗口应该知道任何关于用户控件的viewModel的信息。你能给出(或指向)一个更详细的例子吗? - Dave
@Dave hbarck表示,您不能在控件本身上设置“Name”或“DataContext”(您已经完成或尝试过这两个操作),因为它们会与消费者合理期望设置或继承的值发生冲突(即根据设置位置替换或被替换)。对于克服“Name”和“DataContext”的问题,建议有好的和不太好的(正如您所提出的原因)。为了解决DataContext问题,您应该将其设置在“StackPanel”“LayoutRoot”而不是控件上;外部的“Grid”可以被删除。 - Bob Sammers

1
如果绑定语句以这种方式更改,您将得到什么:
ItemsSource="{Binding ElementName=parent,Path=DataContext.Recordings}"

或者这样写:


ItemsSource="{Binding Recordings}"

如果上述绑定方式解决了当前的绑定错误(“绑定表达式路径错误:未找到'Recordings'属性...”),但导致另一个绑定错误,请发布后一个错误消息。
我认为这部分的正确绑定语句如上所述。
更新:
针对您的编辑进行回应。尝试在StackPanel级别本地设置DataContext,这样您就可以将UserControl设置为不同的DataContext:
public RecordingListControl()
{
    InitializeComponent();
    RecordingListViewModel vm = new RecordingListViewModel();
    LayoutRoot.DataContext = vm;
}

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


感谢您的迅速回复。我尝试了您的建议,并将结果作为对我的主帖进行编辑的报告。您的想法似乎接近,令人着迷...非常感谢,戴夫。 - Dave
1
已更新并提出解决最近绑定错误的建议。 - har07
谢谢har07!问题已解决。我之前以为尝试过了,但显然是出现了错误。已标记为答案并表示感谢。这对我来说是巨大的帮助。 - Dave
不客气。顺便说一句,这是个好问题,我觉得非常清晰(所有相关信息都已发布)。 - har07

0
一个可能的答案是这样的(受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的无聊内容。我认为在标准控件中不需要这样做,那么为什么在这里要这样呢?

1
来自未来的问候。我和你有同样的问题,并且同意你的看法,它会产生无意义的结果。我尝试了在网络上找到的所有其他“解决方案”,但我都不喜欢它们。它们都涉及放弃MVVM的视图模型或改变绑定方式。我已经放弃了Dps,现在只需在我的代码后台添加一行代码即可建立主从链接。 - user585968

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