大而聪明的ViewModel,愚蠢的View和任何模型,最佳的MVVM方法?

8
以下代码是我之前MVVM方法的重构(Fat Models, skinny ViewModels and dumb Views, the best MVVM approach?),在其中将逻辑和INotifyPropertyChanged实现从模型移回到ViewModel中。这更有意义,因为正如所指出的那样,通常您必须使用您无法更改或不想更改的模型,因此您的MVVM方法应能够与任何模型类一起工作,因为它恰好存在。
这个例子仍然允许您在Visual Studio和Expression Blend的设计模式中查看来自模型的实时数据,我认为这很重要,因为您可以有一个模拟数据存储库,设计师连接到该存储库,该存储库具有可能遇到的最小和最大字符串,以便他可以根据这些极端情况调整设计。
问题:
我有些惊讶,因为似乎将“计时器”放在我的ViewModel中是INotifyPropertyChanged的功能,这似乎有些冗余,但这是我能让XAML UI每秒钟反映模型状态的唯一方法。因此,如果您采用这种方法并在后面遇到任何缺点(例如线程或性能),那么听到这些将会很有趣。

如果您只需将XAML和代码后台复制到新的WPF项目中,则以下代码将起作用。

XAML:

<Window x:Class="TestMvvm73892.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:TestMvvm73892"
    Title="Window1" Height="300" Width="300">
    <Window.Resources>
        <ObjectDataProvider 
              x:Key="DataSourceCustomer" 
              ObjectType="{x:Type local:CustomerViewModel}" 
             MethodName="GetCustomerViewModel"/>
    </Window.Resources>

    <DockPanel DataContext="{StaticResource DataSourceCustomer}">
        <StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
            <TextBlock Text="{Binding Path=FirstName}"/>
            <TextBlock Text=" "/>
            <TextBlock Text="{Binding Path=LastName}"/>
        </StackPanel>
        <StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
            <TextBlock Text="{Binding Path=TimeOfMostRecentActivity}"/>
        </StackPanel>

    </DockPanel>

</Window>

代码后台:

using System;
using System.Windows;
using System.ComponentModel;
using System.Threading;

namespace TestMvvm73892
{
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }
    }

    //view model
    public class CustomerViewModel : INotifyPropertyChanged
    {
        private string _firstName;
        private string _lastName;
        private DateTime _timeOfMostRecentActivity;
        private Timer _timer;

        public string FirstName
        {
            get
            {
                return _firstName;
            }
            set
            {
                _firstName = value;
                this.RaisePropertyChanged("FirstName");
            }
        }

        public string LastName
        {
            get
            {
                return _lastName;
            }
            set
            {
                _lastName = value;
                this.RaisePropertyChanged("LastName");
            }
        }

        public DateTime TimeOfMostRecentActivity
        {
            get
            {
                return _timeOfMostRecentActivity;
            }
            set
            {
                _timeOfMostRecentActivity = value;
                this.RaisePropertyChanged("TimeOfMostRecentActivity");
            }
        }

        public CustomerViewModel()
        {
            _timer = new Timer(CheckForChangesInModel, null, 0, 1000);
        }

        private void CheckForChangesInModel(object state)
        {
            Customer currentCustomer = CustomerViewModel.GetCurrentCustomer();
            MapFieldsFromModeltoViewModel(currentCustomer, this);
        }

        public static CustomerViewModel GetCustomerViewModel()
        {
            CustomerViewModel customerViewModel = new CustomerViewModel();
            Customer currentCustomer = CustomerViewModel.GetCurrentCustomer();

            MapFieldsFromModeltoViewModel(currentCustomer, customerViewModel);

            return customerViewModel;
        }

        public static void MapFieldsFromModeltoViewModel
             (Customer model, CustomerViewModel viewModel) 
        {
            viewModel.FirstName = model.FirstName;
            viewModel.LastName = model.LastName;
            viewModel.TimeOfMostRecentActivity = model.TimeOfMostRecentActivity;
        }

        public static Customer GetCurrentCustomer()
        {
            return Customer.GetCurrentCustomer();
        }


        //INotifyPropertyChanged implementation
        public event PropertyChangedEventHandler PropertyChanged;
        private void RaisePropertyChanged(string property)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(property));
            }
        }

    }

    //model
    public class Customer
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public DateTime TimeOfMostRecentActivity { get; set; }

        public static Customer GetCurrentCustomer()
        {
            return new Customer 
                       { FirstName = "Jim"
                         , LastName = "Smith"
                         , TimeOfMostRecentActivity = DateTime.Now 
                       };
        }

    }

}

我已经发布了这个问题,使用了这段代码:如何(正确地)更新WPF应用程序的MVVM中的M - Gennady Vanin Геннадий Ванин
2个回答

15

我喜欢你上面的示例,我认为它实现了MVVM的精神。但是为了澄清,ViewModel代码和Model代码不应该与实际的Code Behind代码在同一个源文件中。事实上,我认为它们不应该在同一个项目中。

以下是我的理解的MVVM:

M - Model是从Business Layer(BL)返回的数据。它应该轻量级,只包含只读数据。Model类是“哑”的,不包含更新、写入或删除逻辑,并且作为请求、命令、操作等的结果由BL生成。Model类不知道消费应用程序的呈现需求,因此可以被任何类型的应用程序使用。为了真正利用这种重用性,我们希望Model类独立于UI项目。

VM - ViewModel包含通信层:它向BL发出请求并以适合展示的方式处理结果。像上面的示例一样,它还接收Model并将其重新格式化以满足特定的呈现需求。把它看作是一个“绑定类”。在上面的示例中,数据只是从一个对象移动到另一个对象,但是ViewModel将负责诸如公开“FullName”类型属性或者在ZipCode前添加前导零等事情。正确的做法是让绑定类来实现INotifyPropertyChanged。同样,为了重用性,我可能会将这一层提取到自己的项目中。这将允许您在不更改基础结构的情况下尝试各种UI选项。

V - View绑定到在VM中创建的绑定类对象。View非常“哑”:它不知道BL或VM。数据可以双向绑定,但是VM处理错误、验证等操作。任何数据同步操作都通过将请求发送回BL并再次处理结果来处理。

这取决于应用程序类型,但似乎经常检查模型是否已更改有些过度。假设您连接到一个从数据访问层(DAL)构建业务对象(BO)的业务逻辑(BL),该DAL正在连接到数据库。在这种情况下,您将不断重建BO,这肯定会影响性能。可以在BL上实现一个监听通知的结帐系统,或者有一个比较上次变化时间与实际时间的方法,或者可以将BO缓存到BL上。这只是一些想法。

另外,我之前说过,模型应该是轻量级的。还有一些重量级选项,例如CSLA,但我不确定它们如何很好地契合MVVM思想。

我并不是想把自己打扮成专家,我只是在设计我们新软件架构的同时研究了这些思想。我很想阅读一些关于这个主题的讨论。


有很多需要考虑的地方,谢谢。是的,我的 MV 和 M 都在代码后台,只是为了方便复制。一个问题:如果你的模型不包含更新、写入和删除方法,那么你会把这些方法放在 ViewModel 中,由“请求、命令、操作”处理吗?但是如果另一个 UI 层需要使用你的模型,它肯定也需要以同样的方式进行更新、写入和删除。 - Edward Tanguay
4
CRUD 方法应该位于业务层。视图模型(VM)只需将请求传递给业务层(BL)。因此,流程如下:视图(V)响应用户输入并触发 ICommand,ICommand 执行 VM 上的方法,VM 发送请求到 BL,BL 返回 M 对象,VM 处理并转换为绑定对象,并将其传递回 V。 - Joel Cochran
哎呀,忘记回答一个问题了:另一个UI层可以以同样的方式使用相同的VM(这也是将其放在自己的项目中的另一个原因)。 - Joel Cochran
你如何在长往返中跟踪V?@JoelCochran - zwcloud
如果与绑定对象相关的 V 尚未创建怎么办?例如,创建一个新模型的 ICommand。 - zwcloud
我认为关键是要理解在你所描述的往返中,M、VM、V和绑定对象的完整生命周期。如果您能更新有关它们生命周期的答案,我将不胜感激。 - zwcloud

3

个人观点是,Model 应用于加载和存储数据,而 ViewModel 的责任则是知道何时需要这些数据,因此在 ViewModel 中使用定时器是有意义的。这样,您就可以使用不同的 ViewModel 与相同的 Model(对于某些 ViewModel,仅检索一次数据即可,而非每秒检索一次)。

需要考虑以下几点:

  • 实现您的模型以支持异步数据检索(如果您想针对 Silverlight 进行开发,则非常重要)
  • 小心从后台线程更新集合(在您的示例中没有问题,但如果您需要使用 ObservableCollection,请记住它无法从非 UI 线程进行更新,在此处阅读更多信息

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