签到有奖
消息提醒
运维工程师专区
官方商城
扫码分享好友 任选多种周边
过节闲着无聊,做了一个和上位机开发有关的小测试。
上位机的开发有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">
<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>
</Window>
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
帖子链接:https://www.ad.siemens.com.cn/club/bbs/post.aspx?a_id=1607129&b_id=5&s_id=0&num=13
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));
}
2.2:ICommand的实现如下。这个测试中也另外用到了Prism的第三方实现的ICommand类库,通过nuget添加。
ICommand中方法的执行需要经过判定,并且判定结果的变化会通过事件通知。我觉得这种比较麻烦就没有采用,而是在ViewModel中采用做为属性的Flag来实现通知和禁用UI元素的机制
using System.Windows.Input;
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;
2.3 一个ViewModel的实现。这里只有一个MainWindow窗口,所以只有一个MainWindowViewModel。
这里同时用两个按钮测试了Prism库中的DelegateCommand,一个附带传string类型参数,另一个传Object类型参数。生产中用Prism的库会更方便,而且有框架直接提供。不过还是需要自己知道底层实现的原理,。
using Microsoft.Win32;
using Prism.Commands;
using System.Windows;
using System.Windows.Controls;
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; }
input1 = value;
this.RaisePropertyChanged("Input1");
private double input2;
public double Input2
get { return input2; }
input2 = value;
this.RaisePropertyChanged("Input2");
private double result;
public double Result
get { return result; }
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
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;
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);
由于采用了MVVM的模式,所以在UI类的实现中,一句代码都不用加。就是默认的形式。
using System.Windows.Data;
using System.Windows.Documents;
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();
3. 事件的传递。WPF中并非所有的界面元素都天然通过依赖性属性可以绑定到ICommand接口,那么就需要找到一个万能的办法。
具体办法就是:对于没有Command依赖属性的UI元素,采用自定义的EXInvokeCommandAction来充当EventTrigger,而这个类实现了Command依赖属性。
3.1 首先需要自定义一个类,用来封装界面的事件相关对象,以做为参数来传递。
namespace WpfApp02
public class ExCommandParameter
public DependencyObject Sender { get; set; }
public EventArgs EventArgs { get; set; }
public object Parameter { get; set; }
3.2 自定义一个扩展的TriggerAction类
using System.Windows.Interactivity;
using System.Reflection;
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
base.ReadPreamble();
return this.commandName;
if (this.CommandName != value)
base.WritePreamble();
this.commandName = value;
base.WritePostscript();
public ICommand Command
return (ICommand)base.GetValue(ExInvokeCommandAction.CommandProperty);
base.SetValue(ExInvokeCommandAction.CommandProperty, value);
public object CommandParameter
return base.GetValue(ExInvokeCommandAction.CommandParameterProperty);
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;
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;
3.3 通过自定义对象,处理View中的事件。这里用了另一个界面来测试,只有一个按钮来触发事件。
界面的Xaml代码如下:
<Window x:Class="WpfApp02.MainWindow"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:local="clr-namespace:WpfApp02"
Title="MainWindow" Height="145" Width="220">
<local:MainWindowViewModel/>
<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>
与MainWindow对应的MainWindowViewModel的C#代码如下:
public class MainWindowViewModel
public ICommand ClickCommand
return new DelegateCommand<ExCommandParameter>((p) =>
RoutedEventArgs args = p.EventArgs as RoutedEventArgs;
MessageBox.Show(args.ToString());
},
(p) =>
);
这里只是简单展示ViewModel得到的来自UI的消息性质。实用中把消息拆包,会根据内容做出不同的响应。
如此的话上位机UI,可以实现与后台业务完全隔离,界面可以随意创新了。
以第一个例子为例,下面是另一个UI界面,直接复制粘贴进去,编译根本不会报错,这就是前后完全分离的好处,因为各自实现的对象是完全没有关联的。
只要很容易的加上几个标准化格式的绑定声明,一分钟搞定运行OK了。
至于后台业务,主要放在Models里面。业务需要的服务放在Service中,这其中包括数据库服务和通信服务。数据库和通信部分都写成通用库放做为底层实现。
分层设计的模式会给开发的带来便利和清晰。
如果对WPF非常熟练的话,其实没必要用VS,用Blend开发相当爽了。按我的理解这并不像很多人认为的Blend是为了前后端分离而搞的,对Xaml非常熟练的人,没理由不用Blend。
补充一点。MVVM模式的好处除了通常提到的UI和后台分离,在真正的项目中,它会让后台的业务本身进行模块化,而这对可维护性和扩展性是巨大的好处。这一点,由于这个案例太小,未能体现。在做过的复杂案例中,这个优点非常明显。
为了实现MVVM,就必须对ViewModel暴露的每一个属性和方法进行精确和标准化的设计和控制。当实现这一点之后就会发现,事实上业务逻辑已经被分隔成诸多的层和小块儿了,每一个的功能都非常单纯明确,这在后台业务进行调整或调试的时候是非常重要的。所以MVVM这种模式,事实上是对来自外界、富于变化的客户需求,真正完成了OO中所说的封装。来自View的全部需求都被非常精致的抽象成了标准接口集,这也就是为什么这个逻辑层被成为ViewModel。
相比之下,底层的数据库和通信,是比较固定的东西,简单的封装即可完成,比较稳定。尤其是通信部分,封装后基本不会随着需求的变化而变化。当然上位机可以根据需求增加不同的通信子模块,以扩展数据源。但每个不同的子模块是非常稳定的。比如和西门子、AB、或者OPC通信等,都会是不同的子模块。
WPF的Xaml语言,对于MVVM模式可以说是量身定做的。为了实现业务逻辑和UI之间的标准接口集,从底层实现来看,需要用到多种不同对象的衔接和分层封装,这在本质上是一个非常繁重的工作。而Xaml用“标记(Markup)”封装了对象,和写网页差不多的几行代码,这些繁重的工作就被瞬间以封装的方式实现和隔离了,再加上她的高性能,还有依赖性属性这么优美的对象之间的衔接方式,不得不说Xaml是个设计优秀的作品。虽说WPF已经多年未更新了,但Xaml这个语言,包括在UWP中,会一直是视觉交互实现的高手。
感谢分享,有什么好的学习资料吗?
我这个菜鸟中的菜鸟也想学学呢
又看一下自定义的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或其它。
我天,好多啊
如果做出的控件嵌入WinCC,如何实现?控件可否与WinCC直接进行数据交换?
分享
扫码分享好友 任选多种好礼
收藏
有帮助
欢迎您访问支持中心!
丰富的视频,全方位的文档,大量的网友交流精华……
为了更好的完善这些内容,我们诚邀您在浏览结束后,花20秒左右的时间,完成一个用户在线调查!
感谢您的支持!
密码至少8位,包含大、小写字母,数字和符号至少三种。
允许邮箱和手机接收来自支持中心网站的信息
我已同意《支持中心网站注册协议和隐私政策》
微信登录扫码一键登录
验证码登录
密码登录
二维码失效点击重试
打开微信扫一扫,快速登录/注册
未注册手机验证后自动登录,注册即代表同意《支持中心网站注册协议和隐私政策》
三日内免验证登录
短信登录
登录