技术论坛

WPF中的MVVM模式

作者 主题
至圣

经验值: 10236
发帖数: 1539
精华帖: 30
主题:【分享】WPF中的MVVM模式
推荐帖


只看楼主 楼主 2020-02-01 21:48:48
标签:

过节闲着无聊,做了一个和上位机开发有关的小测试。

上位机的开发有BS和CS模式,我个人倾向CS,采用.Net上的WPF,可以做漂亮界面。

软件开发存在多种设计模式,这里是用一个小实验来测试和学习一下时下流行的MVVM开发模式。

 

在MVVM模式下,UI(也就是View)是完全和后台剥离的,只通过底层暴露的接口与后台关联。UI只与视觉和交互有关,界面的数据结构被抽象为ViewModel。这样的好处是结构的清晰简化、可扩展性和可维护性很好,尤其是UI的可变化性非常灵活。ViewModel和后台业务,也就是Model、Service 和数据库,进行交互。和PLC的通信属于Service的一部分。

 


其实类似的分离设计模式MVC、MVP等也有一些,MVVM的关键点就在于:ViewModel是和界面去耦合的,它通过事件发布接口数据的变化,而不去调用界面。


MVVM模式的精华部分就在View和ViewModel之间的关系处理,底层业务Model部分其实和其它的分离开发模式一样。在这个简单的小例子中只测试了View和ViewModel之间的实现,没有涉及任何底层的业务,为的是关注重点。

 

1、View部分的代码:这个小测试采用的界面很简单,几个滑杆用于体现数值变化,几个按钮。数值输入,加法结果返回。还有其它几个命令按钮。


<Window x:Class="WpfApp01.MainWindow"

        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

        xmlns:local="clr-namespace:WpfApp01"

        xmlns:vm="clr-namespace:WpfApp01.ViewModels" 

        mc:Ignorable="d"

        Title="MainWindow" Height="350" Width="525">

    

    <Window.DataContext>

        <vm:MainWindowViewModel/>

    </Window.DataContext>


    <Grid>

        <Grid.RowDefinitions>

            <RowDefinition Height="Auto" />

            <RowDefinition Height="*" />

        </Grid.RowDefinitions>

        <Menu>

            <MenuItem Header="_Save" Command="{Binding CommandPopDiag}" IsEnabled="{Binding EnableFlag}" />

        </Menu>

        <Grid Grid.Row="1">

            <Grid.RowDefinitions>

                <RowDefinition Height="Auto" />

                <RowDefinition Height="Auto" />

                <RowDefinition Height="Auto" />

                <RowDefinition Height="*" />

            </Grid.RowDefinitions>

            <Slider x:Name="slider1" Grid.Row="0" Background="LightBlue" Minimum="-100" Maximum="100" Margin="4" 

                    Value="{Binding Input1}"/>

            <Slider x:Name="slider2" Grid.Row="1" Background="LightBlue" Minimum="-100" Maximum="100" Margin="4" 

                    Value="{Binding Input2}"/>

            <Slider x:Name="slider3" Grid.Row="2" Background="LightBlue" Minimum="-100" Maximum="100" Margin="4" 

                    Value="{Binding Result}"/>


            <Button x:Name="addButton" Grid.Row="3" Content="Add" Width="120" Margin="42,20,355,158" 

                    Command="{Binding CommandAdd}" IsEnabled="{Binding EnableFlag}"/> 

            <Button x:Name="MsgButton" Grid.Row="3" Content="Msg" Width="120" Margin="185,20,212,158" 

                Command="{Binding CommandMsg}" CommandParameter="20" IsEnabled="{Binding EnableFlag}"/>

            <Button x:Name="Button2" Grid.Row="3" Content="b" Width="120" Margin="337,20,60,158" 

                Command="{Binding ButtonCommand2}" IsEnabled="{Binding EnableFlag}"

                CommandParameter="{Binding RelativeSource={x:Static RelativeSource.Self}}"/>


            <Button x:Name="LockButton" Grid.Row="3" Content="Lock" Width="120" Margin="185,129,212,48" 

                    Command="{Binding CommandEnable}" />


        </Grid>

    </Grid>

</Window>




 
以下网友喜欢您的帖子:

  
重要声明:

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

帖子链接:https://www.ad.siemens.com.cn/club/bbs/post.aspx?a_id=1607129&b_id=5&s_id=0&num=13

至圣

经验值: 10236
发帖数: 1539
精华帖: 30
回复:WPF中的MVVM测试


只看楼主 楼主 1楼 2020-02-01 21:49:40

2. ViewModel部分:和UI界面的数据交换是通过数据绑定实现的,数据绑定通过INotifyPropertyChange实现通知机制,所有的ViewModel需要继承这个接口的实现。来自界面的命令主要通过ICommand接口实现。

 

2.1 :INotifyPropertyChange接口的实现如下。在WPF中属性数值的变化并不知自动通知界面的,所以需要自己来实现INotifyPropertyChange接口。

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

 

namespace WpfApp01.ViewModels

{

    public class NotificationObject : INotifyPropertyChanged

    {

        public event PropertyChangedEventHandler PropertyChanged;

 

        public void RaisePropertyChanged(string parameter)

        {

            if (this.PropertyChanged != null)

            {

                this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs(parameter));

            }

        }

    }

}



 
以下网友喜欢您的帖子:

  
至圣

经验值: 10236
发帖数: 1539
精华帖: 30
回复:WPF中的MVVM测试


只看楼主 楼主 2楼 2020-02-01 21:51:15

2.2:ICommand的实现如下。这个测试中也另外用到了Prism的第三方实现的ICommand类库,通过nuget添加。


ICommand中方法的执行需要经过判定,并且判定结果的变化会通过事件通知。我觉得这种比较麻烦就没有采用,而是在ViewModel中采用做为属性的Flag来实现通知和禁用UI元素的机制

 

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

using System.Windows.Input;

 

namespace WpfApp01.ViewModels

{

    public class ClassICommand : ICommand

    {        

        public bool CanExecute(object parameter)

        {

                return true;

        }

             

        public event EventHandler CanExecuteChanged;

 

        public void Execute(object parameter)

        {

            if (this.ExcuteAction == null)

            {

                return;

            }

            this.ExcuteAction(parameter);

        }

 

        public Action<object> ExcuteAction;

    }

}



 
以下网友喜欢您的帖子:

  
至圣

经验值: 10236
发帖数: 1539
精华帖: 30
回复:WPF中的MVVM测试


只看楼主 楼主 3楼 2020-02-01 21:54:35

2.3 一个ViewModel的实现。这里只有一个MainWindow窗口,所以只有一个MainWindowViewModel。


这里同时用两个按钮测试了Prism库中的DelegateCommand,一个附带传string类型参数,另一个传Object类型参数。生产中用Prism的库会更方便,而且有框架直接提供。不过还是需要自己知道底层实现的原理,。

 

using Microsoft.Win32;

using Prism.Commands;

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Input;

 

namespace WpfApp01.ViewModels

{

    public class MainWindowViewModel : NotificationObject

    {

        private bool enableFlag = true;

        public bool EnableFlag

        {

            get { return enableFlag; }

            set

            {

                enableFlag = value;

                this.RaisePropertyChanged("EnableFlag");

            }

        }

 

        private double input1;

        public double Input1

        {

            get { return input1; }

            set

            {

                input1 = value;

                this.RaisePropertyChanged("Input1");

            }

        }

 

        private double input2;

        public double Input2

        {

            get { return input2; }

            set

            {

                input2 = value;

                this.RaisePropertyChanged("Input2");

            }

        }

 

        private double result;

        public double Result

        {

            get { return result; }

            set

            {

                result = value;

                this.RaisePropertyChanged("Result");

            }

        }

 

        public ClassICommand CommandAdd { get; set; }

        public ClassICommand CommandPopDiag { get; set; }

        public ClassICommand CommandEnable { get; set; }

 

        public ICommand CommandMsg

        {

            get

            {

                return new DelegateCommand<string>((str) =>

                {

                    MessageBox.Show("Button's parameter:" + str);

                });

            }

        }

 

        public ICommand ButtonCommand2

        {

            get

            {

                return new DelegateCommand<Button>((button) =>

                {

                    MessageBox.Show(button.Content.ToString());

                    if (button.Content.ToString() == "Clicked-1")

                    {

                        button.Content = "Clicked-2";

                    }

                    else

                    {

                        button.Content = "Clicked-1";

                    } 

                });

            }

        }

 

        //Action方法

        private void Add(object parameter)

        {

            this.Result = this.Input1 + this.Input2;

            MessageBox.Show("Working");

        }

        private void PopDiag(object parameter)

        {

            SaveFileDialog dlg = new SaveFileDialog();

            dlg.ShowDialog();

        }

        private void Enabler(object parameter)

        {

            if (!this.EnableFlag)

                this.EnableFlag = true;

            else

                this.EnableFlag = false;

        }        

 

        public MainWindowViewModel()

        {

            this.CommandAdd = new ClassICommand();

            CommandAdd.ExcuteAction = new Action<object>(this.Add);

  

            this.CommandPopDiag = new ClassICommand();

            CommandPopDiag.ExcuteAction = new Action<object>(this.PopDiag);

 

            this.CommandEnable = new ClassICommand();

            CommandEnable.ExcuteAction = new Action<object>(this.Enabler);

        } 

    }

}



 
以下网友喜欢您的帖子:

  
至圣

经验值: 10236
发帖数: 1539
精华帖: 30
回复:WPF中的MVVM测试


只看楼主 楼主 4楼 2020-02-01 21:57:46

由于采用了MVVM的模式,所以在UI类的实现中,一句代码都不用加。就是默认的形式。


using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Data;

using System.Windows.Documents;

using System.Windows.Input;

using System.Windows.Media;

using System.Windows.Media.Imaging;

using System.Windows.Navigation;

using System.Windows.Shapes;


namespace WpfApp01

{

    /// <summary>

    /// Interaction logic for MainWindow.xaml

    /// </summary>

    public partial class MainWindow : Window

    {

        public MainWindow()

        {

            InitializeComponent();

        }

    }

}




 
以下网友喜欢您的帖子:

  
至圣

经验值: 10236
发帖数: 1539
精华帖: 30
回复:WPF中的MVVM测试


只看楼主 楼主 5楼 2020-02-01 22:09:05

3. 事件的传递。WPF中并非所有的界面元素都天然通过依赖性属性可以绑定到ICommand接口,那么就需要找到一个万能的办法。


具体办法就是:对于没有Command依赖属性的UI元素,采用自定义的EXInvokeCommandAction来充当EventTrigger,而这个类实现了Command依赖属性。


3.1 首先需要自定义一个类,用来封装界面的事件相关对象,以做为参数来传递。


using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

using System.Windows;


namespace WpfApp02

{

    public class ExCommandParameter

    {

        public DependencyObject Sender { get; set; }

        public EventArgs EventArgs { get; set; }

        public object Parameter { get; set; }

    }

}





 
以下网友喜欢您的帖子:

  
至圣

经验值: 10236
发帖数: 1539
精华帖: 30
回复:WPF中的MVVM测试


只看楼主 楼主 6楼 2020-02-01 22:18:15

3.2 自定义一个扩展的TriggerAction类


using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Windows;

using System.Windows.Interactivity;

using System.Windows.Input;

using System.Reflection;


namespace WpfApp02

{


public class ExInvokeCommandAction : TriggerAction<DependencyObject>

 {


private string commandName;


public static readonly DependencyProperty CommandProperty = 

DependencyProperty.Register("Command", typeof(ICommand), typeof(ExInvokeCommandAction), null);


public static readonly DependencyProperty CommandParameterProperty = 

DependencyProperty.Register("CommandParameter", typeof(object), typeof(ExInvokeCommandAction), null);


public string CommandName

    {

        get

    {

        base.ReadPreamble();

        return this.commandName;

    }

set

    {

        if (this.CommandName != value)

        {

            base.WritePreamble();

            this.commandName = value;

            base.WritePostscript();

        }

    }

}


public ICommand Command

{

    get

    {

        return (ICommand)base.GetValue(ExInvokeCommandAction.CommandProperty);

    }

    set

    {

        base.SetValue(ExInvokeCommandAction.CommandProperty, value);

    }

}


public object CommandParameter

{

    get

    {

        return base.GetValue(ExInvokeCommandAction.CommandParameterProperty);

    }

    set

    {

        base.SetValue(ExInvokeCommandAction.CommandParameterProperty, value);

    }

}


protected override void Invoke(object parameter)

{

    if (base.AssociatedObject != null)

    {

        ICommand command = this.ResolveCommand();

        ExCommandParameter exParameter = new ExCommandParameter

        {

            Sender = base.AssociatedObject, 

            Parameter = this.GetValue(CommandParameterProperty), 

            EventArgs = parameter as EventArgs  

        };


        if (command != null && command.CanExecute(exParameter)) 

        {

            command.Execute(exParameter);

        }

    }

}


private ICommand ResolveCommand()

{

    ICommand result = null;

    if (this.Command != null)

    {

        result = this.Command; 

    }

    else

    {

        if (base.AssociatedObject != null)

        {

            Type type = base.AssociatedObject.GetType();

            PropertyInfo[] properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public);

            PropertyInfo[] array = properties;

            for (int i = 0; i < array.Length; i++)

            {

                PropertyInfo propertyInfo = array[i];

                if (typeof(ICommand).IsAssignableFrom(propertyInfo.PropertyType) 

                && string.Equals(propertyInfo.Name, this.CommandName, StringComparison.Ordinal))

                {

                    result = (ICommand)propertyInfo.GetValue(base.AssociatedObject, null);

                }

            }

        }

    }

    return result;

}


}

}




 
以下网友喜欢您的帖子:

  
至圣

经验值: 10236
发帖数: 1539
精华帖: 30
回复:WPF中的MVVM测试


只看楼主 楼主 7楼 2020-02-01 22:25:08

3.3 通过自定义对象,处理View中的事件。这里用了另一个界面来测试,只有一个按钮来触发事件。


界面的Xaml代码如下:


<Window x:Class="WpfApp02.MainWindow"

        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"        

        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"        

        xmlns:local="clr-namespace:WpfApp02"

        mc:Ignorable="d"

        Title="MainWindow" Height="145" Width="220">

    <Window.DataContext>

        <local:MainWindowViewModel/>

    </Window.DataContext>

    <Grid>

        <Button Content="Button" Name="button1" VerticalAlignment="Center" Width="100">

            <i:Interaction.Triggers>

                <i:EventTrigger EventName="Click">                    

                    <local:ExInvokeCommandAction 

                        Command="{Binding ClickCommand}"  

                        CommandParameter="{Binding ElementName=button1}" />

                </i:EventTrigger>

            </i:Interaction.Triggers>

        </Button>

    </Grid>

</Window>


与MainWindow对应的MainWindowViewModel的C#代码如下:


using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Windows.Input;

using Prism.Commands;

using System.Windows;


namespace WpfApp02

{

    public class MainWindowViewModel

    {

        public ICommand ClickCommand

        {

            get

            {

                return new DelegateCommand<ExCommandParameter>((p) =>

                {

                    RoutedEventArgs args = p.EventArgs as RoutedEventArgs;

                    MessageBox.Show(args.ToString());

                },

                (p) =>

                {

                    return true;

                }

                );

            }

        }

    }

}


这里只是简单展示ViewModel得到的来自UI的消息性质。实用中把消息拆包,会根据内容做出不同的响应。





 
以下网友喜欢您的帖子:

  
至圣

经验值: 10236
发帖数: 1539
精华帖: 30
回复:WPF中的MVVM测试
推荐帖


只看楼主 楼主 8楼 2020-02-01 22:43:31

如此的话上位机UI,可以实现与后台业务完全隔离,界面可以随意创新了。


以第一个例子为例,下面是另一个UI界面,直接复制粘贴进去,编译根本不会报错,这就是前后完全分离的好处,因为各自实现的对象是完全没有关联的。



只要很容易的加上几个标准化格式的绑定声明,一分钟搞定运行OK了。


至于后台业务,主要放在Models里面。业务需要的服务放在Service中,这其中包括数据库服务和通信服务。数据库和通信部分都写成通用库放做为底层实现。


分层设计的模式会给开发的带来便利和清晰。


如果对WPF非常熟练的话,其实没必要用VS,用Blend开发相当爽了。按我的理解这并不像很多人认为的Blend是为了前后端分离而搞的,对Xaml非常熟练的人,没理由不用Blend。




 
以下网友喜欢您的帖子:

  
至圣

经验值: 10236
发帖数: 1539
精华帖: 30
回复:WPF中的MVVM测试


只看楼主 楼主 9楼 2020-02-04 05:38:49

补充一点。MVVM模式的好处除了通常提到的UI和后台分离,在真正的项目中,它会让后台的业务本身进行模块化,而这对可维护性和扩展性是巨大的好处。这一点,由于这个案例太小,未能体现。在做过的复杂案例中,这个优点非常明显。


为了实现MVVM,就必须对ViewModel暴露的每一个属性和方法进行精确和标准化的设计和控制。当实现这一点之后就会发现,事实上业务逻辑已经被分隔成诸多的层和小块儿了,每一个的功能都非常单纯明确,这在后台业务进行调整或调试的时候是非常重要的。所以MVVM这种模式,事实上是对来自外界、富于变化的客户需求,真正完成了OO中所说的封装。来自View的全部需求都被非常精致的抽象成了标准接口集,这也就是为什么这个逻辑层被成为ViewModel。


相比之下,底层的数据库和通信,是比较固定的东西,简单的封装即可完成,比较稳定。尤其是通信部分,封装后基本不会随着需求的变化而变化。当然上位机可以根据需求增加不同的通信子模块,以扩展数据源。但每个不同的子模块是非常稳定的。比如和西门子、AB、或者OPC通信等,都会是不同的子模块。


WPF的Xaml语言,对于MVVM模式可以说是量身定做的。为了实现业务逻辑和UI之间的标准接口集,从底层实现来看,需要用到多种不同对象的衔接和分层封装,这在本质上是一个非常繁重的工作。而Xaml用标记(Markup)封装了对象,和写网页差不多的几行代码,这些繁重的工作就被瞬间以封装的方式实现和隔离了,再加上她的高性能,还有依赖性属性这么优美的对象之间的衔接方式,不得不说Xaml是个设计优秀的作品。虽说WPF已经多年未更新了,但Xaml这个语言,包括在UWP中,会一直是视觉交互实现的高手。


 
以下网友喜欢您的帖子:

  
版主

经验值: 69053
发帖数: 12266
精华帖: 59
回复:WPF中的MVVM测试


只看楼主 10楼 2020-02-06 16:55:44

感谢分享,有什么好的学习资料吗?

我这个菜鸟中的菜鸟也想学学呢


Q群:https://jq.qq.com/?k=9BDuEgf6
以下网友喜欢您的帖子:

  
至圣

经验值: 10236
发帖数: 1539
精华帖: 30
回复:WPF中的MVVM测试


只看楼主 楼主 12楼 2020-06-03 21:44:42

又看一下自定义的ExInvokeCommandAction类。


这个类的本质:它继承自TriggerAction,所以能和Xaml界面控件的事件绑定,就可以取代系统默认的事件响应方式。并且这个类中的普通属性被封装为依赖性属性,就可以和ViewlModel中的具体命令对象,以及view中的事件对象绑定。其实质就是把ViewModel中的对象带进了View中。让底层的自定义个性化命令,经过标准接口,跨越设计模式的隔离,在View中与界面元素相遇,并完成具体动作。


使用这个类的时候,首先要在ViewModel中,声明出各种ICommand类型的具体命令对象,做为标准接口暴露给UI。这些具体命令对象的形参,必须要匹配源自UI的参数对象的类型。


然后就可以把ExInvokeCommandAction对象,通过Xaml语法绑定到某个UI控件的某个事件,让自己成为这个事件的背后执行机制。


当这个特定事件发生的时候,这个ExInvokeCommandAction对象,会把某个ICommand类型的具体命令对象,通过依赖性属性传递到自己内部,还会把所需的某个界面对象做为参数,也通过依赖性属性传递到自己内部,然后在内部触发Invoke执行。在Invoke内部,界面参数对象被封装为特定格式,传递给特定的具体命令,最后执行这个命令。



这个设计套路审视起来似乎与完美的MvvM标准隔离原则有点不符,因为在界面中加载了底层对象,而这个加载不是通过标准接口进行的。虽然这个对象加载之后,会让UI元素增加绑定接口并扩展背后机制。


在Xaml环境中的Mvvm模式,编程体验上是看不到任何界面元素的背后实现的,在程序集中完全看不到相关代码,看不到designer。这些系统代码当然都存在,只是看不到而已,所以形式上很干净,低耦合。


现在自己做了一个类,并让它成为界面元素的背后机制。既然是背后机制,当然是不走表面的标准接口了。只是自己实现的界面类,是没法让其眼不见为净的。这是个程序集的管理问题,恒定的管理方式可以做为标准的基础。Winform中每个Form背后的designer也是这样的,只是数量多看着有点烦。


INotifyPropertyChanged类型的对象,ICommand类型的对象,再加上扩展的TriggerAction对象,基本上界面和底层之间的对象传递标准化就解决了。


采用MvvM设计模式的一个主要目的在于释放界面的设计创意。界面所需的一切底层信息都被抽象为标准接口集,这是一种封装。这和通常面向对象中常用的把数据对象封装为实体类是一样的,我们通常把这些实体类称为Model。同样道理,由界面View抽象出来的这种数据封装体,当然就叫ViewModel了。


最初触发自己这方面想法的,就是看到美国ICONICS的产品表现,那是靠Xaml实现的。3D、虚拟和增强现实等。Xaml和Xamarin跨平台,都需要这种界面分离的模式。好看好玩的软件才是好软件。之所以没选普遍流行的B/S开发模式,就是因为浏览器中开发不出这么炫的界面,只有客户端才行。当然,如果采用界面分离的模式,底层是稳定的,前脸无所谓是CS还是BS或其它。


 
以下网友喜欢您的帖子:

  
至圣

经验值: 18920
发帖数: 2108
精华帖: 0
回复:WPF中的MVVM模式


只看楼主 13楼 2020-07-07 12:26:52

我天,好多啊


 
以下网友喜欢您的帖子:

  
游侠

经验值: 548
发帖数: 35
精华帖: 0
回复:WPF中的MVVM模式


只看楼主 14楼 2020-07-15 11:16:58

如果做出的控件嵌入WinCC,如何实现?控件可否与WinCC直接进行数据交换?


 
以下网友喜欢您的帖子:

  
  • 上一页
  • 1
  • 下一页
收起
WPF中的MVVM模式
您收到0封站内信:
×
×
信息提示
很抱歉!您所访问的页面不存在,或网址发生了变化,请稍后再试。