基于WPF开发界面的一个很大优势是可以方便地基于MVVM模式开发应用。本文从应用的角度基于MVVM实现参数化管材的创建界面,以体会MVVM给软件架构带来的好处。 基于WPF开发界面的一个很大优势是可以方便地基于MVVM设计模式开发应用。本文从应用的角度基于MVVM实现参数化管材的创建界面。
MVVM是Model-View-ViewModel的简写,即模型-视图-视图模型。网上有若干对MVVM的介绍,本文在此不做过多的赘述,本文将从具体的是应用案例让大家来体会MVVM的优势,即实现UI部分的代码与核心业务逻辑、数据模型分离,达到高耦合低内聚的软件架构目标。

来自网上的截图
我们希望打开一个对话框,在其中可以显示管材模型;修改管材的参数能够实时看到管材形状的变化。如下图所示:

其中管子的外径由管子的内径加上管子壁厚,不需要用户输入。
当然也可以实现用户修改外径,减掉管壁来得到内径。这个可以根据业务需要来调整。
基于MVVM设计模式,我们实现这样的类设计:

其中:
基于XAML实现的UI布局相关代码,即View层;
实现ViewModel层,即View和Model的桥梁,业务逻辑检查,比如半径不能小于0,壁厚不能小于0等。
基于AnyCAD的数据存储类ShapeElement实现Model层。
我们采用自底向上的实现顺序,逐步实现Model、ViewModel和View。
由于是基于AnyCAD内置的组件,可以直接略过。
ShapeElement 可以用来保存TopoShape对象外,可以保存用户自定义的参数。比如管材的长度、内径、厚度等。重点关注以下方法:
//设置参数void SetParameter (String name, ParameterValue val);//查找参数ParameterValue FindParameter (String name);SectionBarVM从INotifyPropertyChanged继承,获得PropertyChanged的能力,即通知View层说:
“嗨,兄弟,该更新界面啦!"
//SectionBarVM.cs public class SectionBarVM : INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; public void OnPropertyChanged(string e) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(e)); } ... }基于属性机制实现。当外部更新,会调用属性set方法的时候,对数据进行合法检查。
若符合要求,更新Model,并调用OnPropertyChanged发起通知。
//SectionBarVM.cs private ShapeElement mModel; public SectionBarVM(ShapeElement model) { mModel = model; } public static string NAME = "Name"; public string Name { get { return mModel.GetName(); } set { if(value != "") { mModel.SetName(value); OnPropertyChanged(NAME); } else { throw new ArgumentException("名称不能为空。"); } } }尺寸参数属性实现:
//SectionBarVM.cs public static string INNER_RADIUS = "InnerRadius"; public static string THICKNESS = "Thickness"; public static string LENGTH = "Length"; public static string OUTTER_RADIUS = "OutterRadius"; public double InnerRadius { get { return ParameterCast.Cast(mModel.FindParameter(INNER_RADIUS), 100.0); } set { if (value > 0) { mModel.SetParameter(INNER_RADIUS, ParameterCreator.Create(value)); OnPropertyChanged(INNER_RADIUS); OnPropertyChanged(OUTTER_RADIUS); } else { throw new ArgumentException("半径太小。"); } } } public double Thickness { get { return ParameterCast.Cast(mModel.FindParameter(THICKNESS), 5.0); } set { if (value > 0) { mModel.SetParameter(THICKNESS, ParameterCreator.Create(value)); OnPropertyChanged(THICKNESS); OnPropertyChanged(OUTTER_RADIUS); } else { throw new ArgumentException("厚度太小。"); } } } public double OutterRadius { get { return InnerRadius + Thickness; } } public double Length { get { return ParameterCast.Cast(mModel.FindParameter(LENGTH), 1000.0); } set { if (value > 0) { mModel.SetParameter(LENGTH, ParameterCreator.Create(value)); OnPropertyChanged(LENGTH); } else { throw new ArgumentException("长度太小。"); } } }这里需要注意的是OutterRadius的实现。由于OutterRadius依赖了InnerRadius和Thickness属性,当被依赖的属性修改后,也需要触发依赖属性的消息。否则界面OutterRadius的值不会再更新。
增加一个窗口AddSectionBarDlg.xaml,按照设计要求进行布局。
Path="InnerRadius"将会跟SectionBarVM的InnerRadius绑定。当UI修改的时候会调用InnerRadius set; 当界面初始化和数据更新的时候,UI会调用InnerRadius get。
<TextBox Width="150"> <Binding Path="InnerRadius"> <Binding.ValidationRules> <ExceptionValidationRule/> </Binding.ValidationRules> </Binding> </TextBox>Mode="OneWay" 表示UI只会从ViewModel获取数据。
<TextBox Width="150" IsEnabled="False"> <Binding Path="OutterRadius" Mode="OneWay"> </Binding> </TextBox>XAML完整代码:
//AddSectionBarDlg.xaml<Window x: 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:Rapid.Sketch.Plugin.UI" xmlns:anycad="clr-namespace:AnyCAD.WPF;assembly=AnyCAD.WPF.NET6" mc:Ignorable="d" Title="创建型材" Height="450" Width="650" ResizeMode="NoResize" Icon="/Rapid.Common.Res;component/Image/SectionBar.png"> <Grid Margin="7"> <Grid.ColumnDefinitions> <ColumnDefinition Width="400"></ColumnDefinition> <ColumnDefinition Width="Auto"></ColumnDefinition> </Grid.ColumnDefinitions> <anycad:RenderControl Name="mView3d" Grid.Column="0" ViewerReady="MView3d_ViewerReady"/> <Grid Grid.Column="1" Margin="7"> <Grid.RowDefinitions> <RowDefinition Height="360"></RowDefinition> <RowDefinition Height="28"></RowDefinition> </Grid.RowDefinitions> <StackPanel Grid.Row="0"> <StackPanel Orientation="Horizontal"> <Label Width="60" Content="名称:"></Label> <TextBox Width="150"> <Binding Path="Name"> </Binding> </TextBox> </StackPanel> <StackPanel Orientation="Horizontal" Margin="0,7,0,0"> <Label Width="60" Content="内径:"></Label> <TextBox Width="150"> <Binding Path="InnerRadius"> <Binding.ValidationRules> <ExceptionValidationRule/> </Binding.ValidationRules> </Binding> </TextBox> </StackPanel> <StackPanel Orientation="Horizontal" Margin="0,7,0,0"> <Label Width="60" Content="厚度:"></Label> <TextBox Width="150"> <Binding Path="Thickness"> <Binding.ValidationRules> <ExceptionValidationRule/> </Binding.ValidationRules> </Binding> </TextBox> </StackPanel> <StackPanel Orientation="Horizontal" Margin="0,7,0,0"> <Label Width="60" Content="外径:"></Label> <TextBox Width="150" IsEnabled="False"> <Binding Path="OutterRadius" Mode="OneWay"> </Binding> </TextBox> </StackPanel> <StackPanel Orientation="Horizontal" Margin="0,7,0,0"> <Label Width="60" Content="长度:"></Label> <TextBox Width="150"> <Binding Path="Length"> <Binding.ValidationRules> <ExceptionValidationRule/> </Binding.ValidationRules> </Binding> </TextBox> </StackPanel> </StackPanel> <StackPanel Orientation="Horizontal" Grid.Row="1" HorizontalAlignment="Right" Margin="0,0,0,7"> <Button Content="取消" Width="60" Margin="7,0,7,0"></Button> <Button Content="确定" Width="60" Margin="7,0,7,0"></Button> </StackPanel> </Grid> </Grid></Window>把ViewModel对象设置给Window的DataContext属性,即可实现UI与ViewModel的关联。
另外我们希望更改数据后也能更新三维窗口,在这里我们先用比较笨的办法实现,即硬编码实现参数与三维模型的联动。详见SbVM_PropertyChanged方法的实现。
/// <summary> /// AddSectionBarDlg.xaml 的交互逻辑 /// </summary> public partial class AddSectionBarDlg : Window { SectionBarVM m_Bar; public AddSectionBarDlg(SectionBarVM sbVM) { InitializeComponent(); this.Owner = App.Current.MainWindow; this.DataContext = sbVM; sbVM.PropertyChanged += SbVM_PropertyChanged; m_Bar = sbVM; } private void SbVM_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { if(e.PropertyName == SectionBarVM.THICKNESS || e.PropertyName == SectionBarVM.INNER_RADIUS || e.PropertyName == SectionBarVM.LENGTH) { mView3d.View3D.ClearAll(); var shape = m_Bar.CreateShape(); mView3d.ShowShape(shape, ColorTable.LightGrey); mView3d.View3D.ZoomAll(1.6f); } } private void MView3d_ViewerReady() { mView3d.View3D.SetBackgroundColor(30.0f / 255, 30.0f / 255, 30.0f / 255, 0); var shape = m_Bar.CreateShape(); mView3d.ShowShape(shape, ColorTable.LightGrey); mView3d.View3D.ZoomAll(1.6f); } }暂时在草图项目中增加一个按钮,可以调用对话框:
<Fluent:RibbonGroupBox Header="型材" IsLauncherVisible="False" Margin="7,0,0,0"> <Fluent:Button Header="管材" Icon="/Rapid.Common.Res;component/Image/SectionBar.png" Size="Large" Command="{x:Static local:SketchRibbonTab.ExecuteCommand}" CommandParameter="pipeTube" Margin="0,0,7,0"/> </Fluent:RibbonGroupBox> case "pipeTube": { //临时创建一个对象 var se = new ShapeElement(); se.SetName("管子"); var dlg = new AddSectionBarDlg(new SectionBarVM(se)); dlg.ShowDialog(); }运行效果:

从实现代码的结构来看,使用MVVM设计模式确实可以让代码层次更清楚,界面类不再臃肿不堪。Microsoft设计XAML之初的一个目标是希望做UI布局的UX与写代码逻辑的开发能够分工协作,甚至为此开发了独立的设计工具Blend给UX使用,以让开发能够直接重用UX实现的XAML……
虽然现实并没有想象的那么美好,但基于MVVM模式确实可以实现界面布局和核心业务逻辑分离,甚至把不同层的功能分给不同水平的程序员来实现。