.NET6: 开发基于WPF的摩登三维工业软件 (8) - MVVM
基于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对象外,可以保存用户自定义的参数。比如管材的长度、内径、厚度等。重点关注以下方法:
//设置参数
void SetParameter (String name, ParameterValue val);
//查找参数
ParameterValue FindParameter (String name);
4.2 ViewModel实现
4.2.1 更新界面的能力
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));
}
...
}
4.2.2 更新数据能力
基于属性机制实现。当外部更新,会调用属性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的值不会再更新。
4.3 View实现
4.3.1 界面布局
增加一个窗口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:Class="Rapid.Sketch.Plugin.UI.AddSectionBarDlg"
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>
4.3.2 View与ViewModel绑定
把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);
}
}
5 功能集成
暂时在草图项目中增加一个按钮,可以调用对话框:
<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();
}
运行效果:

6 总结
从实现代码的结构来看,使用MVVM设计模式确实可以让代码层次更清楚,界面类不再臃肿不堪。Microsoft设计XAML之初的一个目标是希望做UI布局的UX与写代码逻辑的开发能够分工协作,甚至为此开发了独立的设计工具Blend给UX使用,以让开发能够直接重用UX实现的XAML……
虽然现实并没有想象的那么美好,但基于MVVM模式确实可以实现界面布局和核心业务逻辑分离,甚至把不同层的功能分给不同水平的程序员来实现。
.NET6: 开发基于WPF的摩登三维工业软件 (8) - MVVM的更多相关文章
- .NET6: 开发基于WPF的摩登三维工业软件 (2)
在<.NET6: 开发基于WPF的摩登三维工业软件 (1)>我们创建了一个"毛坯"界面,距离摩登还差一段距离.本文将对上一阶段的成果进行深化,实现当下流行的暗黑风格UI ...
- .NET6: 开发基于WPF的摩登三维工业软件 (7)
做为一个摩登的工业软件,提供可编程的脚本能力是必不可少的能力.脚本既可以方便用户进行二次开发,也对方便对程序进行自动化测试.本文将结合AnyCAD对Python脚本支持的能力和WPF快速开发带脚本编辑 ...
- .NET6: 开发基于WPF的摩登三维工业软件
MS Office和VisualStudio一直引领着桌面应用的时尚潮流,大型的工业软件一般都会紧跟潮流,搭配着Ribbon和DockPanel风格的界面.本文将介绍WPF下两个轻量级的Ribbon和 ...
- .NET6: 开发基于WPF的摩登三维工业软件 (10) - 机器人
基于前文介绍的Ribbon界面.插件化.MVVM模式等内容,我们搭建了一个软件雏形.本文将综合之前的内容在RapidCAX框架中集成Robot组件,实现一个简单的机器人正向模拟模块. 1 目标 基于M ...
- 封装:简要介绍自定义开发基于WPF的MVC框架
原文:封装:简要介绍自定义开发基于WPF的MVC框架 一.目的:在使用Asp.net Core时,深感MVC框架作为页面跳转数据处理的方便,但WPF中似乎没有现成的MVC框架,由此自定义开发一套MVC ...
- (转)基于 WPF + Modern UI 的 公司OA小助手 开发总结
原文地址:http://www.cnblogs.com/rainlam163/p/3365181.html 前言: 距离上一篇博客,整整一个月的时间了.人不能懒下来,必须有个阶段性的总结,算是对我这个 ...
- 基于 WPF + Modern UI 的 公司OA小助手 开发总结
前言: 距离上一篇博客,整整一个月的时间了.人不能懒下来,必须有个阶段性的总结,算是对我这个阶段的一个反思.人只有在总结的过程中才会发现自己的不足. 公司每天都要在OA系统上上班点击签到,下班点击签退 ...
- 【基于WPF+OneNote+Oracle的中文图片识别系统阶段总结】之篇二:基于OneNote难点突破和批量识别
篇一:WPF常用知识以及本项目设计总结:http://www.cnblogs.com/baiboy/p/wpf.html 篇二:基于OneNote难点突破和批量识别:http://www.cnblog ...
- 快速开发基于 HTML5 网络拓扑图应用
采用 HT 开发网络拓扑图非常容易,例如<入门手册>的第一个小例子麻雀虽小五脏俱全:http://www.hightopo.com/guide/guide/core/beginners/e ...
随机推荐
- APC 篇——总结与提升
写在前面 此系列是本人一个字一个字码出来的,包括示例和实验截图.由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新. 如有好的建议,欢迎反馈.码字不易, ...
- SP419/422 TRANSP(2) - Transposing is Fun
首先可以发现转置本质上就是一个置换,问题就转化为求一个排列排成有序的最少次数. 这是一个经典问题,答案为点数减循环置换的个数,考虑如何求循环置换. 发现有两个特殊性质:置换为转置,矩阵的边长为 \(2 ...
- 使用estimatedRowHeight的优缺点
使用estimatedRowHeight的优缺点 1.优点 1> 可以降低tableView:heightForRowAtIndexPath:方法的调用频率 2> 将[计算cell高度的操 ...
- centos7 安装yum源
centos7的服务器,没有yum,没有wget命令真的寸步难行,经过总结和查询,现在算搞定了,把经验总结一下,以免大家再浪费时间去找原因: 安装yum需要wget安装yum的rpm包,所以前提是先有 ...
- Javascript 生成全局唯一标识符 (GUID,UUID)
全局唯一标识符(GUID,Globally Unique Identifier)也称作 UUID(Universally Unique IDentifier) . GUID是一种由算法生成的二进制长度 ...
- shell脚本三剑客之awk
shell脚本之awk命令 AWK 是一种处理文本文件的语言,是一个强大的文本分析工具 适合小型文本数据 1.工作原理 2.AWK格式 3.按行输入文本 4.按字段输入文本 5.通过管道符号,双引号调 ...
- springCloud项目解决跨域问题
通过 spring cloud gateway 实现, 方式一:选择在主启动类中注册 CorsWebFilter 类: /** * 1.允许cookies跨域 * 2.允许向该服务器提交请求的URI, ...
- [LeetCode]14.最长公共前缀(Java)
原题地址: longest-common-prefix 题目描述: 编写一个函数来查找字符串数组中的最长公共前缀. 如果不存在公共前缀,返回空字符串 "". 示例 1: 输入:st ...
- linux系统中实用shell脚本,请收藏!
1.Dos攻击防范(自动屏蔽攻击 IP) #!/bin/bashDATE=$(date +%d/%b/%Y:%H:%M)LOG_FILE=/usr/local/nginx/logs/demo2.acc ...
- [LeetCode]1431. 拥有最多糖果的孩子
给你一个数组 candies 和一个整数 extraCandies ,其中 candies[i] 代表第 i 个孩子拥有的糖果数目. 对每一个孩子,检查是否存在一种方案,将额外的 extraCandi ...