从自定义 UserControl 的 DependencyProperty 返回值并使用 viewModel 的正确方法?

问题描述 投票:0回答:1

我将自定义用户控件包装到 dll,并希望引用它的其他人能够通过绑定到“Result”属性来获取或设置结果值。
这是 CustomControl.cs 代码:

public partial class CustomControl : UserControl
{
    public IEnumerable<string> Result
    {
        get { return (IEnumerable<string>)GetValue(resultProperty); }
        set { SetValue(resultProperty, value); }
    }
    public static readonly DependencyProperty resultProperty = DependencyProperty.Register("Result", typeof(IEnumerable<string>), typeof(CustomControl), 
        new FrameworkPropertyMetadata(null,FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, ResultChangedEvent));
    
    private static void ResultChangedEvent(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var uc = d as CustomControl;
        if (uc == null) return;
        uc.viewModel.InitResult((IEnumerable<string>)e.NewValue);
    }

    //There's actually another attribute here called 'Source'
    //....

    internal CustomControlViewModel viewModel;
    public CustomControl()
    {
        InitializeComponent();
        ContentGrid.DataContext = viewModel = new CustomControlViewModel();
    }
}

自定义ControlViewModel.cs
这里的 SelectedList 也会被其他事件处理,它应该返回最终列表作为“结果”数据。

internal class CustomControlViewModel
{
    public ObservableCollection<string> SelectedList { get; set; } = new ObservableCollection<string>();
    //public ObservableCollection<string> SourceList { get; set; } 

    public void InitResult(IEnumerable<string> items)
    {
        SelectedList.Clear();
        foreach (var name in items.Distinct())
            SelectedList.Add(name);
    }       
 
    public void OtherEventHandleList(string text)
    {
        // select other item from SourceList
        //SelectedList.Add(text);
    }
}

外部引用UserControl的代码:
MainWindowViewModel.cs:

class MainWindowViewModel
{    
    public ObservableCollection<string> RawList { get; set; } = new ObservableCollection<string>()
    { "data1", "data2", "data3" , "data4" };
    public ObservableCollection<string> ResultList { get; set; } = new ObservableCollection<string>()
    { "data1", "data2" };

    public void GetResult()
    {
        MessageBox.Show(string.Join(",", ResultList));
    }
}

MainWindow.xaml

<Window xmlns:cc="clr-namespace:CustomControl;assembly=CustomControl">
    <Grid>
        <cc:TagSelectControl Result="{Binding ResultList}" /><!-- Source="{Binding RawList}"-->
    </Grid>
</Window>

但是这个UserControl的Result Binding只有在赋值时才有效,我一直找不到合适的方法让Result返回SelectedList。
如何让 DependencyProperty 返回 viewModel 的属性?

c# wpf mvvm viewmodel dependency-properties
1个回答
0
投票
  1. 因为您在构造函数内显式设置了

    DataContext
    ,所以您破坏了针对您的
    DataContext
    的外部绑定的
    UserControl
    。出于这个原因以及可重用性的原因,您永远不会在内部设置自定义控件的
    DataContext
    。控件必须始终忽略其当前的
    DataContext

  2. 控件不应包含任何数据逻辑。它不应该计算数据并返回任何结果。
    控件是应用程序视图的模块。视图唯一的责任是显示数据并与用户交互,例如收集输入或修改数据。它不生成数据。
    任何计算和数据处理都必须在视图模型(如果与数据表示相关)或模型(如果与业务逻辑相关)中进行。

自定义控件必须不关心它的

DataContext
。如果您的控件需要外部数据,您必须引入外部
DataContext
(例如您的
MainViewModel
)可以绑定到的依赖属性。

除此之外,您没有每个控件的视图模型,而是每个数据范围的视图模型。当您遵循设计自定义控件的常见做法时,每个控件的视图模型将自动消除 - 即不要让您的控件依赖于

DataContext

MVVM 是一种应用程序架构设计模式。视图模型描述应用程序组件而不是控制组件。应用程序视图模型由许多类组成。以某种方式,使用
ViewModel
后缀命名这些根类(在类层次结构内)成为一种约定。这些根类通常描述视图的数据范围。多个控件通常共享相同的
DataContext

从 MVVM 角度来看,控件的所有逻辑都与 UI 相关,因此是视图的一部分。它不属于任何视图模型。相反,将其移至“代码隐藏”(C# 代码)。这些类不是视图模型类。

如果您正确设计控件,您甚至不必关心源集合的类型。就像

ListBox
可以在不知道数据类型的情况下显示任何数据。它通过不关心实际数据项来实现这一点。它只是加载项目容器,将数据项目分配给项目容器的
DataContext
,并将布局委托给客户端。然后,客户端将定义一个
DataTemplate
来告诉项目容器它们如何呈现。

您可以非常轻松地复制此行为。

以下示例展示了如何修改代码以使其与数据和数据上下文无关:

自定义控件.xaml.cs

public partial class CustomControl : UserControl
{
  public IList Results
  {
    get => (IList)GetValue(ResultsProperty);
    set => SetValue(ResultsProperty, value);
  }

  // A TwoWay binding on an collection is redundant
  // because your control will never change the instance.
  // It only displays the values of the current instance.
  // The instance is provided by the external view model.
  public static readonly DependencyProperty ResultsProperty = DependencyProperty.Register(
    "Results",
    typeof(IList),
    typeof(CustomControl),
    new FrameworkPropertyMetadata(default));

  // Enable the client to template the item the way you would do with an ItemsControl
  public DataTemplate ResultTemplate
  {
    get => (DataTemplate)GetValue(ResultTemplateProperty);
    set => SetValue(ResultTemplateProperty, value);
  }

  public static readonly DependencyProperty ResultTemplateProperty = DependencyProperty.Register(
    "ResultTemplate",
    typeof(DataTemplate),
    typeof(CustomControl),
    new PropertyMetadata(default));

  public CustomControl()
  { 
    InitializeComponent();
  }
}

自定义控件.xaml

<UserControl x:Name="Root">
  <ListBox ItemsSource="{Binding ElementName=Root, Path=Results}"
           ItemTemplate="{Binding ElementName=Root, Path=ResultTemplate}" />
</UserControl>

MainViewModel.cs
重要的是,您的视图模型(或一般的非 DependencyObject 绑定源)实现

INotifyPropertyChanged
- 即使它们不更改属性值。

将代码从

CustomControlViewModel
移至应用程序视图模型(例如
MainViewModel
类)。如果它包含数据逻辑,则它不属于视图。那么它就属于视图模型或模型。

class MainWindowViewModel : INotifyPropertyChanged
{
  public ObservableCollection<string> RawList { get; set; } = new ObservableCollection<string>()
  { "data1", "data2", "data3" , "data4" };
  public ObservableCollection<string> ResultList { get; set; } = new ObservableCollection<string>()
  { "data1", "data2" }; 
    
  private ObservableCollection<string> SelectedList { get; set; } = new ObservableCollection<string>();
  //public ObservableCollection<string> SourceList { get; set; } 

  public void GetResult()
  {
    MessageBox.Show(string.Join(",", ResultList));
  }
}

MainWindow.xaml

<Window>
  <Window.DataContext>
    <MainViewModel />
  </Window.DataContext>

  <cc:TagSelectControl Results="{Binding ResultList}">
    <cc:TagSelectControl.ResultTemplate>
      <DataTemplate>
        <TextBlock Text="{Binding}" />
      </DataTemplate>
    </cc:TagSelectControl.ResultTemplate>
  </cc:TagSelectControl>
</Window>

如果您只想显示项目列表并要求自定义控件具有某些特殊行为,那么推荐的方法是扩展

ItemsControl
(或
ListBox
)而不是
UserControl

自定义控件.xaml.cs

public partial class CustomControl : ListBox
{
}

MainWindow.xaml

<Window>
  <Window.DataContext>
    <MainViewModel />
  </Window.DataContext>

  <cc:TagSelectControl ItemsSource="{Binding ResultList}">
    <cc:TagSelectControl.ItemTemplate>
      <DataTemplate>
        <TextBlock Text="{Binding}" />
      </DataTemplate>
    </cc:TagSelectControl.ItemTemplate>
  </cc:TagSelectControl>
</Window>
© www.soinside.com 2019 - 2024. All rights reserved.