• .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模式确实可以实现界面布局和核心业务逻辑分离,甚至把不同层的功能分给不同水平的程序员来实现。

  • 相关阅读:
    学习进度笔记01
    进度报表十一
    进度报表十
    进度日报九
    进度日报八
    进度日报七
    第七周总结
    进度报表六
    第一阶段冲刺6
    第一阶段冲刺5
  • 原文地址:https://www.cnblogs.com/anycad/p/mvvm.html
Copyright © 2020-2023  润新知