基于WPF开发界面的一个很大优势是可以方便地基于MVVM设计模式开发应用。本文从应用的角度基于MVVM实现参数化管材的创建界面。

1 MVVM

MVVM是Model-View-ViewModel的简写,即模型-视图-视图模型。网上有若干对MVVM的介绍,本文在此不做过多的赘述,本文将从具体的是应用案例让大家来体会MVVM的优势,即实现UI部分的代码与核心业务逻辑、数据模型分离,达到高耦合低内聚的软件架构目标。

来自网上的截图

2 界面设计

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

其中管子的外径由管子的内径加上管子壁厚,不需要用户输入。

当然也可以实现用户修改外径,减掉管壁来得到内径。这个可以根据业务需要来调整。

3 程序设计

基于MVVM设计模式,我们实现这样的类设计:

其中:

  • AddSectionBarDlg

基于XAML实现的UI布局相关代码,即View层;

  • SectionBarVM

实现ViewModel层,即View和Model的桥梁,业务逻辑检查,比如半径不能小于0,壁厚不能小于0等。

  • ShapeElement

基于AnyCAD的数据存储类ShapeElement实现Model层。

4 程序实现

我们采用自底向上的实现顺序,逐步实现Model、ViewModel和View。

4.1 Model实现

由于是基于AnyCAD内置的组件,可以直接略过。

ShapeElement 可以用来保存TopoShape对象外,可以保存用户自定义的参数。比如管材的长度、内径、厚度等。重点关注以下方法:

  1. //设置参数
  2. void SetParameter (String name, ParameterValue val);
  3. //查找参数
  4. ParameterValue FindParameter (String name);

4.2 ViewModel实现

4.2.1 更新界面的能力

SectionBarVM从INotifyPropertyChanged继承,获得PropertyChanged的能力,即通知View层说:

“嗨,兄弟,该更新界面啦!"

  1. //SectionBarVM.cs
  2. public class SectionBarVM : INotifyPropertyChanged
  3. {
  4. public event PropertyChangedEventHandler? PropertyChanged;
  5. public void OnPropertyChanged(string e)
  6. {
  7. if (PropertyChanged != null)
  8. PropertyChanged(this, new PropertyChangedEventArgs(e));
  9. }
  10. ...
  11. }

4.2.2 更新数据能力

基于属性机制实现。当外部更新,会调用属性set方法的时候,对数据进行合法检查。

若符合要求,更新Model,并调用OnPropertyChanged发起通知。

  1. //SectionBarVM.cs
  2. private ShapeElement mModel;
  3. public SectionBarVM(ShapeElement model)
  4. {
  5. mModel = model;
  6. }
  7. public static string NAME = "Name";
  8. public string Name {
  9. get { return mModel.GetName(); }
  10. set {
  11. if(value != "")
  12. {
  13. mModel.SetName(value);
  14. OnPropertyChanged(NAME);
  15. }
  16. else
  17. {
  18. throw new ArgumentException("名称不能为空。");
  19. }
  20. }
  21. }

尺寸参数属性实现:

  1. //SectionBarVM.cs
  2. public static string INNER_RADIUS = "InnerRadius";
  3. public static string THICKNESS = "Thickness";
  4. public static string LENGTH = "Length";
  5. public static string OUTTER_RADIUS = "OutterRadius";
  6. public double InnerRadius {
  7. get { return ParameterCast.Cast(mModel.FindParameter(INNER_RADIUS), 100.0); }
  8. set {
  9. if (value > 0)
  10. {
  11. mModel.SetParameter(INNER_RADIUS, ParameterCreator.Create(value));
  12. OnPropertyChanged(INNER_RADIUS);
  13. OnPropertyChanged(OUTTER_RADIUS);
  14. }
  15. else
  16. {
  17. throw new ArgumentException("半径太小。");
  18. }
  19. }
  20. }
  21. public double Thickness {
  22. get { return ParameterCast.Cast(mModel.FindParameter(THICKNESS), 5.0); }
  23. set {
  24. if (value > 0)
  25. {
  26. mModel.SetParameter(THICKNESS, ParameterCreator.Create(value));
  27. OnPropertyChanged(THICKNESS);
  28. OnPropertyChanged(OUTTER_RADIUS);
  29. }
  30. else
  31. {
  32. throw new ArgumentException("厚度太小。");
  33. }
  34. }
  35. }
  36. public double OutterRadius
  37. {
  38. get { return InnerRadius + Thickness; }
  39. }
  40. public double Length {
  41. get { return ParameterCast.Cast(mModel.FindParameter(LENGTH), 1000.0); }
  42. set {
  43. if (value > 0)
  44. {
  45. mModel.SetParameter(LENGTH, ParameterCreator.Create(value));
  46. OnPropertyChanged(LENGTH);
  47. }
  48. else
  49. {
  50. throw new ArgumentException("长度太小。");
  51. }
  52. }
  53. }

这里需要注意的是OutterRadius的实现。由于OutterRadius依赖了InnerRadius和Thickness属性,当被依赖的属性修改后,也需要触发依赖属性的消息。否则界面OutterRadius的值不会再更新。

4.3 View实现

4.3.1 界面布局

增加一个窗口AddSectionBarDlg.xaml,按照设计要求进行布局。

  • 数据双向绑定

Path="InnerRadius"将会跟SectionBarVM的InnerRadius绑定。当UI修改的时候会调用InnerRadius set; 当界面初始化和数据更新的时候,UI会调用InnerRadius get。

  1. <TextBox Width="150">
  2. <Binding Path="InnerRadius">
  3. <Binding.ValidationRules>
  4. <ExceptionValidationRule/>
  5. </Binding.ValidationRules>
  6. </Binding>
  7. </TextBox>
  • 数据单向绑定

Mode="OneWay" 表示UI只会从ViewModel获取数据。

  1. <TextBox Width="150" IsEnabled="False">
  2. <Binding Path="OutterRadius" Mode="OneWay">
  3. </Binding>
  4. </TextBox>

XAML完整代码:

  1. //AddSectionBarDlg.xaml
  2. <Window x:Class="Rapid.Sketch.Plugin.UI.AddSectionBarDlg"
  3. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  4. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  5. xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  6. xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  7. xmlns:local="clr-namespace:Rapid.Sketch.Plugin.UI"
  8. xmlns:anycad="clr-namespace:AnyCAD.WPF;assembly=AnyCAD.WPF.NET6"
  9. mc:Ignorable="d"
  10. Title="创建型材" Height="450" Width="650" ResizeMode="NoResize" Icon="/Rapid.Common.Res;component/Image/SectionBar.png">
  11. <Grid Margin="7">
  12. <Grid.ColumnDefinitions>
  13. <ColumnDefinition Width="400"></ColumnDefinition>
  14. <ColumnDefinition Width="Auto"></ColumnDefinition>
  15. </Grid.ColumnDefinitions>
  16. <anycad:RenderControl Name="mView3d" Grid.Column="0" ViewerReady="MView3d_ViewerReady"/>
  17. <Grid Grid.Column="1" Margin="7">
  18. <Grid.RowDefinitions>
  19. <RowDefinition Height="360"></RowDefinition>
  20. <RowDefinition Height="28"></RowDefinition>
  21. </Grid.RowDefinitions>
  22. <StackPanel Grid.Row="0">
  23. <StackPanel Orientation="Horizontal">
  24. <Label Width="60" Content="名称:"></Label>
  25. <TextBox Width="150">
  26. <Binding Path="Name">
  27. </Binding>
  28. </TextBox>
  29. </StackPanel>
  30. <StackPanel Orientation="Horizontal" Margin="0,7,0,0">
  31. <Label Width="60" Content="内径:"></Label>
  32. <TextBox Width="150">
  33. <Binding Path="InnerRadius">
  34. <Binding.ValidationRules>
  35. <ExceptionValidationRule/>
  36. </Binding.ValidationRules>
  37. </Binding>
  38. </TextBox>
  39. </StackPanel>
  40. <StackPanel Orientation="Horizontal" Margin="0,7,0,0">
  41. <Label Width="60" Content="厚度:"></Label>
  42. <TextBox Width="150">
  43. <Binding Path="Thickness">
  44. <Binding.ValidationRules>
  45. <ExceptionValidationRule/>
  46. </Binding.ValidationRules>
  47. </Binding>
  48. </TextBox>
  49. </StackPanel>
  50. <StackPanel Orientation="Horizontal" Margin="0,7,0,0">
  51. <Label Width="60" Content="外径:"></Label>
  52. <TextBox Width="150" IsEnabled="False">
  53. <Binding Path="OutterRadius" Mode="OneWay">
  54. </Binding>
  55. </TextBox>
  56. </StackPanel>
  57. <StackPanel Orientation="Horizontal" Margin="0,7,0,0">
  58. <Label Width="60" Content="长度:"></Label>
  59. <TextBox Width="150">
  60. <Binding Path="Length">
  61. <Binding.ValidationRules>
  62. <ExceptionValidationRule/>
  63. </Binding.ValidationRules>
  64. </Binding>
  65. </TextBox>
  66. </StackPanel>
  67. </StackPanel>
  68. <StackPanel Orientation="Horizontal" Grid.Row="1" HorizontalAlignment="Right" Margin="0,0,0,7">
  69. <Button Content="取消" Width="60" Margin="7,0,7,0"></Button>
  70. <Button Content="确定" Width="60" Margin="7,0,7,0"></Button>
  71. </StackPanel>
  72. </Grid>
  73. </Grid>
  74. </Window>

4.3.2 View与ViewModel绑定

把ViewModel对象设置给Window的DataContext属性,即可实现UI与ViewModel的关联。

另外我们希望更改数据后也能更新三维窗口,在这里我们先用比较笨的办法实现,即硬编码实现参数与三维模型的联动。详见SbVM_PropertyChanged方法的实现。

  1. /// <summary>
  2. /// AddSectionBarDlg.xaml 的交互逻辑
  3. /// </summary>
  4. public partial class AddSectionBarDlg : Window
  5. {
  6. SectionBarVM m_Bar;
  7. public AddSectionBarDlg(SectionBarVM sbVM)
  8. {
  9. InitializeComponent();
  10. this.Owner = App.Current.MainWindow;
  11. this.DataContext = sbVM;
  12. sbVM.PropertyChanged += SbVM_PropertyChanged;
  13. m_Bar = sbVM;
  14. }
  15. private void SbVM_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
  16. {
  17. if(e.PropertyName == SectionBarVM.THICKNESS ||
  18. e.PropertyName == SectionBarVM.INNER_RADIUS ||
  19. e.PropertyName == SectionBarVM.LENGTH)
  20. {
  21. mView3d.View3D.ClearAll();
  22. var shape = m_Bar.CreateShape();
  23. mView3d.ShowShape(shape, ColorTable.LightGrey);
  24. mView3d.View3D.ZoomAll(1.6f);
  25. }
  26. }
  27. private void MView3d_ViewerReady()
  28. {
  29. mView3d.View3D.SetBackgroundColor(30.0f / 255, 30.0f / 255, 30.0f / 255, 0);
  30. var shape = m_Bar.CreateShape();
  31. mView3d.ShowShape(shape, ColorTable.LightGrey);
  32. mView3d.View3D.ZoomAll(1.6f);
  33. }
  34. }

5 功能集成

暂时在草图项目中增加一个按钮,可以调用对话框:

  1. <Fluent:RibbonGroupBox Header="型材" IsLauncherVisible="False" Margin="7,0,0,0">
  2. <Fluent:Button Header="管材" Icon="/Rapid.Common.Res;component/Image/SectionBar.png" Size="Large" Command="{x:Static local:SketchRibbonTab.ExecuteCommand}"
  3. CommandParameter="pipeTube" Margin="0,0,7,0"/>
  4. </Fluent:RibbonGroupBox>
  1. case "pipeTube":
  2. {
  3. //临时创建一个对象
  4. var se = new ShapeElement();
  5. se.SetName("管子");
  6. var dlg = new AddSectionBarDlg(new SectionBarVM(se));
  7. dlg.ShowDialog();
  8. }

运行效果:

6 总结

从实现代码的结构来看,使用MVVM设计模式确实可以让代码层次更清楚,界面类不再臃肿不堪。Microsoft设计XAML之初的一个目标是希望做UI布局的UX与写代码逻辑的开发能够分工协作,甚至为此开发了独立的设计工具Blend给UX使用,以让开发能够直接重用UX实现的XAML……

虽然现实并没有想象的那么美好,但基于MVVM模式确实可以实现界面布局和核心业务逻辑分离,甚至把不同层的功能分给不同水平的程序员来实现。

.NET6: 开发基于WPF的摩登三维工业软件 (8) - MVVM的更多相关文章

  1. .NET6: 开发基于WPF的摩登三维工业软件 (2)

    在<.NET6: 开发基于WPF的摩登三维工业软件 (1)>我们创建了一个"毛坯"界面,距离摩登还差一段距离.本文将对上一阶段的成果进行深化,实现当下流行的暗黑风格UI ...

  2. .NET6: 开发基于WPF的摩登三维工业软件 (7)

    做为一个摩登的工业软件,提供可编程的脚本能力是必不可少的能力.脚本既可以方便用户进行二次开发,也对方便对程序进行自动化测试.本文将结合AnyCAD对Python脚本支持的能力和WPF快速开发带脚本编辑 ...

  3. .NET6: 开发基于WPF的摩登三维工业软件

    MS Office和VisualStudio一直引领着桌面应用的时尚潮流,大型的工业软件一般都会紧跟潮流,搭配着Ribbon和DockPanel风格的界面.本文将介绍WPF下两个轻量级的Ribbon和 ...

  4. .NET6: 开发基于WPF的摩登三维工业软件 (10) - 机器人

    基于前文介绍的Ribbon界面.插件化.MVVM模式等内容,我们搭建了一个软件雏形.本文将综合之前的内容在RapidCAX框架中集成Robot组件,实现一个简单的机器人正向模拟模块. 1 目标 基于M ...

  5. 封装:简要介绍自定义开发基于WPF的MVC框架

    原文:封装:简要介绍自定义开发基于WPF的MVC框架 一.目的:在使用Asp.net Core时,深感MVC框架作为页面跳转数据处理的方便,但WPF中似乎没有现成的MVC框架,由此自定义开发一套MVC ...

  6. (转)基于 WPF + Modern UI 的 公司OA小助手 开发总结

    原文地址:http://www.cnblogs.com/rainlam163/p/3365181.html 前言: 距离上一篇博客,整整一个月的时间了.人不能懒下来,必须有个阶段性的总结,算是对我这个 ...

  7. 基于 WPF + Modern UI 的 公司OA小助手 开发总结

    前言: 距离上一篇博客,整整一个月的时间了.人不能懒下来,必须有个阶段性的总结,算是对我这个阶段的一个反思.人只有在总结的过程中才会发现自己的不足. 公司每天都要在OA系统上上班点击签到,下班点击签退 ...

  8. 【基于WPF+OneNote+Oracle的中文图片识别系统阶段总结】之篇二:基于OneNote难点突破和批量识别

    篇一:WPF常用知识以及本项目设计总结:http://www.cnblogs.com/baiboy/p/wpf.html 篇二:基于OneNote难点突破和批量识别:http://www.cnblog ...

  9. 快速开发基于 HTML5 网络拓扑图应用

    采用 HT 开发网络拓扑图非常容易,例如<入门手册>的第一个小例子麻雀虽小五脏俱全:http://www.hightopo.com/guide/guide/core/beginners/e ...

随机推荐

  1. ApacheCN 深度学习译文集 20201218 更新

    新增了四个教程: Python 人工智能中文版 0 前言 1 人工智能简介 2 人工智能的基本用例 3 机器学习管道 4 特征选择和特征工程 5 使用监督学习的分类和回归 6 集成学习的预测分析 7 ...

  2. 【XR-4】文本编辑器

    直接做是困难的,不妨依照部分分来思考. - Subtask 3 首先会进入一个误区:维护修改,通过循环串的性质在 \(\tt KMP\) 自动机上优化遍历. 但可以发现这样很难处理,我们不妨 直接维护 ...

  3. windows10使用wireshark抓取本机请求包

    1.管理员运行cmd 右键左下角windows图标,管理员运行Windows PowerShell 2.输入ipconfg查看本机ip和网关ip 3.执行命令 route add 本机ip mask ...

  4. bom案例3-放大镜

    <!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8&quo ...

  5. Android下数据库操作——增删改查

    Android下数据库第一种方式增删改查     1.创建一个帮助类的对象,调用getReadableDatabase方法,返回一个SqliteDatebase对象     2.使用SqliteDat ...

  6. C语言设计ATM存取款界面

    这个虽然很简单,但是我还是弄了一会儿,可见有多菜.练习算. 主要考察switch开关语句.do...while语句,页面的跳转我用的是goto,虽然是弊端,可是还是用了,因为太菜啊.大家有好建议的欢迎 ...

  7. ◆JAVA加密解密-DES

    DES算法提供CBC, OFB, CFB, ECB四种模式,MAC是基于ECB实现的. 一.数据补位 DES数据加解密就是将数据按照8个字节一段进行DES加密或解密得到一段8个字节的密文或者明文,最后 ...

  8. iframe父子页面相互调用方法,相互获取元素

    父页面获取子页面 var childWin = document.getElementById('setIframe').contentWindow;//获取子页面窗口对象 childWin.send ...

  9. Scala变量和数据类型

    一.注释及代码规范 Scala的注释和Java中完全相同:单行注释:// .多行注释:/* */ 以及文档注释:/**   */: 使用tab操作,实现缩进,默认整体向右边移动,用shift+tab整 ...

  10. Ubuntu18修改/迁移mysql5.7数据存放路径

    1.停止mysql服务 sudo service mysql stop 2.修改mysql配置文件,一般是 /etc/mysql/my.cnf,或者/etc/mysql/mysql.conf.d/my ...