如何在 UserControl 中扩展模型?

Posted

技术标签:

【中文标题】如何在 UserControl 中扩展模型?【英文标题】:How do I extend a model in a UserControl? 【发布时间】:2021-01-16 02:52:36 【问题描述】:

我想了解如何在 WPF 中编写适当的用户控件/视图模型。为了简单起见,我发明了以下示例:

说,我们有一个DateRange 类,定义为:

using System;

namespace MyDateApp

public class DateRange

    public DateTime Start
    
        get;
        set;
     = new DateTime();
    public int Length // in days
    
        get;
        set;
     = 0;


(为了论证,我们假设这个类不能以任何方式修改。)

类的一个实例被用作我们窗口的数据上下文:

using System;
using System.Windows;

namespace MyDateApp

public partial class MainWindow : Window

    public MainWindow()
    
        InitializeComponent();
        DateRange range = new DateRange();
        range.Start = new DateTime(2020, 1, 1);
        range.Length = 5;
        DataContext = range;
    


我想实现一个自定义视图/控件,它允许应用程序的用户选择开始和结束日期,而不是开始日期和范围。它将被包裹在一个名为DateControlUserControl 中:

<Window x:Class="MyDateApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:MyDateApp">
    <StackPanel>
        <local:DateControl Range="Binding" />
    </StackPanel>
</Window>

我能够得到这个DateControl 工作的基本实现:

using System;
using System.Windows;
using System.Windows.Controls;

namespace MyDateApp

public partial class DateControl : UserControl

    public DateControl()
    
        InitializeComponent();
    
    public DateRange Range
    
        get  return (DateRange)GetValue(RangeProperty); 
        set  SetValue(RangeProperty, value); 
    
    public static readonly DependencyProperty RangeProperty =
        DependencyProperty.Register("Range", typeof(DateRange), typeof(DateControl), new PropertyMetadata(new DateRange()));
    public DateTime End
    
        get => Range.Start + new TimeSpan(Range.Length, 0, 0, 0);
        set => Range.Length = (value - Range.Start).Days;
    


使用以下 XAML:

<UserControl x:Class="MyDateApp.DateControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:MyDateApp"
             Name="UserControl">
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="From:" />
        <DatePicker SelectedDate="Binding Range.Start, ElementName=UserControl" />
        <TextBlock Text="To:" />
        <DatePicker SelectedDate="Binding End, ElementName=UserControl" />
    </StackPanel>
</UserControl>

它非常适合开始日期,但是结束日期的值是错误的。因此,我的问题:

我怎样才能正确地实现这个?

我的成功条件是:

原始模型类必须保持不变, 并且用户控件必须更新主窗口数据上下文。

奖励: 我必须实现什么才能将此控件用作ListBoxItem

【问题讨论】:

DataContext 会自动传递给您的 UserControl。不需要依赖属性。因此,您可以简单地绑定到 ViewModel 中的属性 Start 和 End 回答您的条件:“原始模型类必须保持不变”,然后可能从模型继承。保持 UI 尽可能的愚蠢。否则你总是依赖你的 UI 来处理业务逻辑。第二:这不是你的模型,而是你的视图模型。 @Klamsi “DataContext 会自动传递给您的 UserControl。”是什么意思?我可以在不绑定主窗口的情况下执行&lt;DatePicker SelectedDate="Binding Range.Start, ElementName=UserControl" /&gt; 吗? @Klamsi 我想分离会很好,但我的业务逻辑必须存在于某个地方......我不确定如何执行你的想法。 【参考方案1】:

创建一个位于视图和模型之间的视图模型,并将视图中的控件绑定到视图模型,而不是直接绑定到视图,例如:

public class ViewModel

    //error handling and validation omitted for brevity...

    private readonly DateRange _model = new DateRange();

    private DateTime _startDate;
    public DateTime StartDate
    
        get
        
            return _model.Start;
        
        set 
        
            _model.Start = value;
        
    

    private DateTime _endDate;
    public DateTime EndDate
    
        get  return _endDate; 
        set 
         
            _endDate = value;
            _model.Length = (int)_endDate.Date.Subtract(_startDate.Date).TotalDays;
        
    

【讨论】:

到目前为止这是有道理的,但是我如何将这个视图模型与其余代码一起插入以使其工作?特别是,XAML 应该是什么样子? XAML 中的控件只能绑定到视图模型属性。或者将控件属性绑定到视图模型属性,将控件绑定到控件属性:&lt;local:DateControl Start="Binding StartDate" ... /&gt; 是的,但我必须在某个地方从我的模型转换为我的视图模型。我在哪里做呢? 视图模型具有对模型实例的引用并处理“转换”。视图不知道模型。 啊。是的。你说的对!但是,这不适用于 List&lt;DateRange&gt; 列表,因为数据是“绑定”在视图模型中的?【参考方案2】:

将模型直接暴露给视图通常是个坏主意。

这包括“包装”模型属性的方法。

这适用于琐碎的应用程序,但您会发现许多边缘情况使其没有吸引力。 例如,您的用户编辑模型类。 这验证失败。 现在,您的模型中有错误数据。 它也相当笨重。

我建议将您的模型类视为 DTO。

构建具有相应属性的视图模型以及您将不可避免地需要的额外属性。

实例化该视图模型并从模型中复制数据以将其呈现给视图。

数据库>模型>视图模型>视图

在编辑/插入时执行相反的操作并实例化模型以传回您的存储库或实体框架。

视图 > 视图模型 > 模型 > 数据库

这样,您的数据注释可以愉快地存在于视图模型中,而不会污染模型。如果您使用的是实体框架等 ORM,这将特别方便。

对于特别复杂的场景,您甚至可能需要另一层用于特定领域逻辑的类。

要直接复制属性,请考虑使用 automapper。

https://automapper.org/

编辑:

使用这种方法,视图模型与模型完全断开,因此视图不会改变它。从而满足要求1。

此视图模型应由主窗口视图模型实例化为私有成员。主窗口的公共属性将被设置为一个实例。对于列表,属性将是这些视图模型的列表或 observablecollection。

我会考虑删除用户控件中的依赖属性,而是将逻辑放入新的视图模型中。

我不遵循这个逻辑到底是什么,但我希望你不会对你设计的每个视图模型提出问题,这里的任何答案都应该表明这个原则。

因此视图模型可能看起来像:

public class DateRangeViewModel : BindableBase

    public DateRangeViewModel(DateTime _datefrom, int _length)
    
        length = _length;
        fromDate = _datefrom;
        setUpDate();
    
    private void setUpDate()
    
        ToDate = ((DateTime)fromDate).AddDays(length);
    

    private int length = 0;

    public int Length
    
        get => length;
        set  setUpDate(); SetProperty(ref length, value, nameof(Length)); 
    
    private DateTime? fromDate;

    public DateTime? FromDate
    
        get => fromDate;
        set => SetProperty(ref fromDate, value, nameof(FromDate));
    

    private DateTime? toDate;

    public DateTime? ToDate
    
        get => toDate;
        set  setUpDate(); SetProperty(ref fromDate, value, nameof(ToDate)); 
    
 

您的 windowviewmodel 会读取您的数据,这些数据来自您将其作为模型的任何地方。

为您新建一个 DateRangeViewModel,将 fromdate 和 length 属性传递给 ctor。

【讨论】:

我遇到了你之前所说的基本要点。但是,我的问题是,虽然我可能在概念层面上理解它,但我无法将其放入代码中。视频创作者和博客作者都忽略了所有这些小的实现细节。这些实际上可能是微不足道的,但之前没有看到它们实现,我无法真正理解这些概念。因此,请把你的文章写成能解决我的特定例子的代码。 迄今为止我发现的最好的教程来自“Angelsix”(youtube)。他制作了一系列关于 WPF UI 编程的文章。

以上是关于如何在 UserControl 中扩展模型?的主要内容,如果未能解决你的问题,请参考以下文章

使用 MVVM 在 MainWindow 上绑定 UserControl 视图模型

如何从作为wpf mvvm模式中的窗口打开的视图模型中关闭用户控件?

Winform异步初始化UserControl的问题

我可以编写一个 .NETCF 部分类来扩展 System.Windows.Forms.UserControl 吗?

为啥我不能在我的 UserControl 中重置 TextBox 的背景?

从 Catel WPF UserControl 中的 ResourceDictionary 中绑定