【问题标题】:How do I extend a model in a UserControl?如何在 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 我想分离会很好,但我的业务逻辑必须存在于某个地方......我不确定如何执行你的想法。

标签: c# wpf xaml mvvm user-controls


【解决方案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 编程的文章。
猜你喜欢
  • 1970-01-01
  • 2016-09-06
  • 1970-01-01
  • 1970-01-01
  • 2013-11-10
  • 2018-05-11
  • 1970-01-01
  • 2014-05-11
  • 1970-01-01
相关资源
最近更新 更多