我将自定义用户控件包装到 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 的属性?
因为您在构造函数内显式设置了
DataContext
,所以您破坏了针对您的 DataContext
的外部绑定的 UserControl
。出于这个原因以及可重用性的原因,您永远不会在内部设置自定义控件的 DataContext
。控件必须始终忽略其当前的 DataContext
。
控件不应包含任何数据逻辑。它不应该计算数据并返回任何结果。
控件是应用程序视图的模块。视图唯一的责任是显示数据并与用户交互,例如收集输入或修改数据。它不生成数据。
任何计算和数据处理都必须在视图模型(如果与数据表示相关)或模型(如果与业务逻辑相关)中进行。
自定义控件必须不关心它的
DataContext
。如果您的控件需要外部数据,您必须引入外部 DataContext
(例如您的 MainViewModel
)可以绑定到的依赖属性。
除此之外,您没有每个控件的视图模型,而是每个数据范围的视图模型。当您遵循设计自定义控件的常见做法时,每个控件的视图模型将自动消除 - 即不要让您的控件依赖于
DataContext
。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>