欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

从0到1:使用Caliburn.Micro(WPF和MVVM)开发简单的计算器

程序员文章站 2022-04-28 21:50:51
从0到1:使用Caliburn.Micro(WPF和MVVM)开发简单的计算器 之前时间一直在使用Caliburn.Micro这种应用了MVVM模式的WPF框架做开发,是时候总结一下了。 Caliburn.Micro(Caliburn.Micro框架概述 https://blog.csdn.net/ ......

从0到1:使用caliburn.micro(wpf和mvvm)开发简单的计算器

之前时间一直在使用caliburn.micro这种应用了mvvm模式的wpf框架做开发,是时候总结一下了。

caliburn.micro(caliburn.micro框架概述 - ) 是一个轻量级的wpf框架,简化了wpf中的不少用法,推荐做wpf开发时优先使用。

真正快速而熟练地掌握一门技术就可以尝试着用最快的速度去构建一个玩具项目(toy project),然后不断地优化、重构之。比如本文将介绍如何使用caliburn.micro v3.2开发出一个简单的计算器,里面用到了c#中的async异步技术,caliburn.micro中的conductor等等~

step 1: 在vs中创建wpf项目

从0到1:使用Caliburn.Micro(WPF和MVVM)开发简单的计算器

step 2: 使用nuget包管理工具为当前项目安装caliburn.micro

对于caliburn.micro 1.x和2.x版,只能使用.dll,需手动给项目加reference。而3.0以后的版本可使用nuget包管理工具来管理,安装和卸载既方便又彻底,推荐使用。(ps: nuget之于visual studio(c++, c#等), 犹pip之于python, npm之于node, maven之于java, gem之于ruby等等)

从0到1:使用Caliburn.Micro(WPF和MVVM)开发简单的计算器

step 3: 框架搭建

  1. 删除项目根目录下的mainwindow.xaml
  2. 按下图调整app.xaml
    删除语句startupuri="mainwindow.xmal"。
    从0到1:使用Caliburn.Micro(WPF和MVVM)开发简单的计算器

  3. 填充application.resources
    <application.resources>
         <resourcedictionary>
             <resourcedictionary.mergeddictionaries>
                 <resourcedictionary>
                     <local:bootstrapper x:key="bootstrapper"/>
                 </resourcedictionary>
             </resourcedictionary.mergeddictionaries>
         </resourcedictionary>
    </application.resources>

   4 . 创建bootstrapper类
然后让其继承自bootstrapperbase类,并加上构造函数,另外再重写函数onstartup即可。

using system.windows;
using caliburn.micro;
using caliburnmicro_calculator.viewmodels;

namespace caliburnmicro_calculator
{
    public class bootstrapper : bootstrapperbase
    {
        public bootstrapper()
        {
            initialize();
        }

        protected override void onstartup(object obj, startupeventargs e)
        {
            displayrootviewfor<shellviewmodel>();
        }
    }
}

   5 . 在项目目录下新建models, viewmodels, views这3个文件夹
在viewmodel文件夹中添加shellviewmodel.cs,并创建left, right和result这3个属性。

需要注意的是 shellviewmodel.cs需要继承类 screen 和 inotifypropertychanged (用于感知并同步所绑定属性的变化),shellviewmodel具体代码为:

using system.componentmodel;
using system.threading;
using system.windows;
using system.windows.controls;
using caliburn.micro;

namespace caliburnmicro_calculator.viewmodels
{
    public class shellviewmodel : screen, inotifypropertychanged
    {
        private double _left;
        private double _right;
        private double _result;

        public double left
        {
            get { return _left; }
            set
            {
                _left = value;
                notifyofpropertychange();
            }
        }

        public double right
        {
            get { return _right; }
            set
            {
                _right = value;
                notifyofpropertychange();
            }
        }

        public double result
        {
            get { return _result; }
            set
            {
                _result = value;
                notifyofpropertychange();
            }
        }
}

说明: 最开始布局xaml时,设计位置时采用的是左(operand 1), 中(operand 2), 右(result),于是属性值使用了left, right和result。

step 4: 设计xaml并绑定属性

在views文件夹中创建window,命名为shellview.xaml,在views文件夹下创建子文件夹images,用于存放+,-,*,/这4种操作对应的小图标,其具体代码如下:

<window x:class="caliburnmicro_calculator.views.shellview"
        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:caliburnmicro_calculator.views"
        xmlns:cal="http://www.caliburnproject.org"
        mc:ignorable="d"
        title="calculator" sizetocontent="height" width="240">

    <stackpanel background="beige">
        <stackpanel orientation="horizontal">
            <label margin="10"
                   target="{binding elementname=left}">
                operand _1:
            </label>
            <textbox margin="10"
                     width="72"
                     x:name="left"/>
        </stackpanel>
        <stackpanel orientation="horizontal">
            <label margin="10"
                   target="{binding elementname=right}">
                operand _2:
            </label>
            <textbox margin="10"
                     width="72"
                     x:name="right"/>
        </stackpanel>
        <stackpanel orientation="horizontal">
            <button margin="10"
                    x:name="btnplus" 
                    cal:message.attach="[event click]=[action plus(left.text, right.text):result.text]">
                <image source="images/op1.ico"/>
            </button>

            <button margin="10"
                    x:name="btnminus" 
                    cal:message.attach="[event click]=[action minus(left.text, right.text):result.text]">
                <image source="images/op2.ico"/>
            </button>

            <button margin="10"
                    x:name="btnmultiply" 
                    cal:message.attach="[event click]=[action multipy(left.text, right.text):result.text]">
                <image source="images/op3.ico"/>
            </button>

            <button margin="10"
                    x:name="btndivide" isenabled="{binding path=candivide}"
                    cal:message.attach="[event click]=[action divide(left.text, right.text):result.text]">
                <image source="images/op4.ico"/>
            </button>

        </stackpanel>
        <stackpanel orientation="horizontal">
            <label margin="10">
                answer:
            </label>
            <textbox margin="10"
                     width="72"
                     text ="{binding path=result, stringformat={}{0:f4}}" isreadonly="true" />
        </stackpanel>
    </stackpanel>
</window>

说明:对操作数operand _1和operand _2,按alt键+数字可以选中该处,这是wpf的一个特殊用法。由于计算结果不希望被修改,于是加上了属性isreadonly="true"

step 5: 设计并绑定事件

由于暂时只打算实现+, -, *, /四种操作,于是我们只需创建相应的4个函数即可,由于除数是0这个操作不允许,于是需再加个判断函数candivide。

caliburn.micro中绑定事件的写法是:
cal:message.attach="[event e]=[action a]"(e是操作,比如click, mousedown, keydown等等,a是viewmodel中具体的函数。)

向shellviewmodel中加入事件中要做的事,此时shellviewmodel为:

using system.componentmodel;
using system.threading;
using system.windows;
using system.windows.controls;
using caliburn.micro;

namespace caliburnmicro_calculator.viewmodels
{
    public class shellviewmodel : screen, inotifypropertychanged
    {
        private double _left;
        private double _right;
        private double _result;

        public double left
        {
            get { return _left; }
            set
            {
                _left = value;
                notifyofpropertychange();
            }
        }

        public double right
        {
            get { return _right; }
            set
            {
                _right = value;
                notifyofpropertychange();
            }
        }

        public double result
        {
            get { return _result; }
            set
            {
                _result = value;
                notifyofpropertychange();
            }
        }
                public bool candivide(double left, double right)
        {
            return right != 0;
        }

        public async void divide(double left, double right)
        {
            thread.sleep(600);
            if (candivide(left, right) == true)
                result = left / right;
            else messagebox.show("divider cannot be zero.", "warning", messageboxbutton.ok, messageboximage.warning);
        }

        public async void plus(double left, double right)
        {
            result = left + right;
        }

        public async void minus(double left, double right)
        {
            result = left - right;
        }

        public async void multipy(double left, double right)
        {
            result = left * right;
        }
    }
}

此时计算器的功能已基本完成,但我们可以对viewmodel进行适当的调整:
1.创建新的viewmodel - calculatorviewmodel,将原来的shellviewmodel中具体的计算逻辑移入到calculatorviewmodel中;
2.此时让shellviewmodel继承conductor<object>,于是shellviewmodel拥有了管理screen实例的功能(viewmodel中使用activateitem函数,而view中使用x:name="activateitem"标签),其具体代码为:

using system.componentmodel;
using system.threading;
using system.windows;
using system.windows.controls;
using caliburn.micro;

namespace caliburnmicro_calculator.viewmodels
{
    public class shellviewmodel : conductor<object>
    {
        public shellviewmodel()
        {
        }
        public void showcalculator()
        {
            activateitem(new calculatorviewmodel());
        }
    }
}

此时,calculatorviewmodel的具体代码为:

using system.componentmodel;
using system.threading;
using system.windows;
using caliburn.micro;

namespace caliburnmicro_calculator.viewmodels
{
    public class calculatorviewmodel: screen, inotifypropertychanged
    {
        private double _left;
        private double _right;
        private double _result;

        public double left
        {
            get { return _left; }
            set
            {
                _left = value;
                notifyofpropertychange();
            }
        }

        public double right
        {
            get { return _right; }
            set
            {
                _right = value;
                notifyofpropertychange();
            }
        }

        public double result
        {
            get { return _result; }
            set
            {
                _result = value;
                notifyofpropertychange();
            }
        }

        public calculatorviewmodel()
        {
        }

        public bool candivide(double left, double right)
        {
            return right != 0;
        }

        public async void divide(double left, double right)
        {
            thread.sleep(600);
            if (candivide(left, right) == true)
                result = left / right;
            else messagebox.show("divider cannot be zero.", "warning", messageboxbutton.ok, messageboximage.warning);
        }

        public async void plus(double left, double right)
        {
            result = left + right;
        }

        public async void minus(double left, double right)
        {
            result = left - right;
        }

        public async void multipy(double left, double right)
        {
            result = left * right;
        }
    }
}

  3 . 对于view,只需把calculatorviewmodel对应的calculatorview作为contentcontrol控件嵌入shellview即可。此时shellview的代码调整为:

<window x:class="caliburnmicro_calculator.views.shellview"
        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:caliburnmicro_calculator.views"
        xmlns:cal="http://www.caliburnproject.org"
        mc:ignorable="d"
        title="calculator" sizetocontent="height" width="240">

    <grid minheight="200">
        <button content="show calculator" x:name="showcalculator" grid.row="0"></button>
        <contentcontrol x:name="activeitem"></contentcontrol>        
    </grid>
</window>

另外提一点,向viewmodel a中嵌入viewmodel b,一般来说需要做的操作是:
在a的view中使用contentcontrol,绑定b的viewmodel只需使用语句cal:view.model="{binding bviewmodel}"即可,而b的view是usercontrol就可以啦。

此时calculatorview是一个usercontrol,其代码为:

<usercontrol x:class="caliburnmicro_calculator.views.calculatorview"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:caliburnmicro_calculator.views"
             xmlns:cal="http://www.caliburnproject.org"
             mc:ignorable="d"
             width="240">

    <stackpanel background="beige">
        <stackpanel orientation="horizontal">
            <label margin="10"
                   target="{binding elementname=left}">
                operand _1:
            </label>
            <textbox margin="10"
                     width="72"
                     x:name="left"/>
        </stackpanel>
        <stackpanel orientation="horizontal">
            <label margin="10"
                   target="{binding elementname=right}">
                operand _2:
            </label>
            <textbox margin="10"
                     width="72"
                     x:name="right"/>
        </stackpanel>
        <stackpanel orientation="horizontal" horizontalalignment="center">
            <button margin="10"
                    x:name="btnplus" 
                    cal:message.attach="[event click]=[action plus(left.text, right.text):result.text]">
                <image source="images/op1.ico"/>
            </button>

            <button margin="10"
                    x:name="btnminus" 
                    cal:message.attach="[event click]=[action minus(left.text, right.text):result.text]">
                <image source="images/op2.ico"/>
            </button>

            <button margin="10"
                    x:name="btnmultiply" 
                    cal:message.attach="[event click]=[action multipy(left.text, right.text):result.text]">
                <image source="images/op3.ico"/>
            </button>

            <button margin="10"
                    x:name="btndivide" isenabled="{binding path=candivide}"
                    cal:message.attach="[event click]=[action divide(left.text, right.text):result.text]">
                <image source="images/op4.ico"/>
            </button>

        </stackpanel>
        <stackpanel orientation="horizontal">
            <label margin="10">
                answer:
            </label>
            <textbox margin="10"
                     width="72"
                     text ="{binding path=result, stringformat={}{0:f4}, updatesourcetrigger=propertychanged}" isreadonly="true" />
        </stackpanel>
    </stackpanel>
</usercontrol>

好啦,就酱,由于本例中逻辑并不复杂,model暂时用不上,对于复杂一点的项目,model主要负责数据的读取,如文件操作、数据库操作、service调用等,以后有机会举例具体来说。

如果需要持久化(persistent),则还需给给每对m-vm(model和viewmodel)加入state,这个实际工程中也用得特别多。

part 6: 功能举例

calculator主页:
从0到1:使用Caliburn.Micro(WPF和MVVM)开发简单的计算器

点击按钮“showcalculator”即可看到具体的计算器~

乘法举例:
从0到1:使用Caliburn.Micro(WPF和MVVM)开发简单的计算器

除法举例:
从0到1:使用Caliburn.Micro(WPF和MVVM)开发简单的计算器

最后附上代码:
caliburnmicro-calculator: a simple calculator using caliburn.micro
https://github.com/yanglr/caliburnmicro-calculator
欢迎fork和star,如有改进意见欢迎提交pull request~