分享一个WinForm下实现MVVM的简易框架

bluedoctor 2016-11-14 04:07:57
加精
Web前端技术的大力发展,各种跨平台的基于HTML5的移动前端开发技术逐渐成熟,各种应用逐步由传统的C/S 转换到 B/S ,APP模式,基于C/S模式的前端技术比如WPF的关注度逐渐下降,因此WPF上的MVVM并不是应用得很广,目前很多遗留的或者新的 C/S系统仍然采用WinForms技术开发维护,然而WinForms 上却没有良好的MVVM框架,WinForms 的UI效果和整体开发质量,开发效率没有得到有效提高,要过度到WPF开发这种不同开发风格的技术难度又比较大,所以,如果有一种能够在 WinForms 上的MVVM框架,无疑是广大后端.NET程序员的福音。

笔者一直是一个奋斗在一线的.NET开发人员,架构师,对于Web 和桌面,后端开发技术都有广泛的涉及,深刻理解开发人员自嘲自己为“码农”的心理的,工作辛苦又没有时间陪女朋友陪家人,所以我一直总结整理如何提高开发效率,改善开发质量的方法,经过近10年的时间,发展完善了一套开发框架—SOD框架。最近研究改善Web前端开发的技术,Vue.js框架的MVVM思想再一次让我觉得WinForms上MVVM技术的必要性,发现要实现MVVM框架其实并不难,关键在于模型(Model)和视图(View)的双向绑定,即模型的改变引起视图内容的改变,而视图的改变也能够引起模型的改变。

SOD框架的实体类可以直接用来作为MVVM上的Model提供给View 做为被绑定对象,因此要我们只需要解决WinForms 形式的View 元素如何实现绑定操作,那么我们的WinForms 应用即可实现MVVM功能了。在WinForms 上,控件基本上都已经实现了绑定功能,它就是控件的 DataBindings,向它添加绑定即可,例如下面的例子:

this.textbox1.DataBindings.Add("Text", userEntity, "Name");


这样当文本框架输入的内容改变后,实体类对象 userEntity.Name 属性的值也会改变。如果userEntity是SOD实体类,所以userEntity.Name 改变,文本框的Text属性也会同步改变。

使用SOD的WinForms MVVM框架,只需要在 code behind 写如下类似的代码:

SubmitedUsersViewModel DataContext{get;set;}

private void Form1_Load(object sender, EventArgs e)
{
base.BindDataControls(this.Controls);
base.BindCommandControls(this.button1, DataContext.SubmitCurrentUsers);
base.BindCommandControls(this.button2, DataContext.UpdateUser);
base.BindCommandControls(this.button3, DataContext.RemoveUser);
}


详细使用过程,请看此文《“老坛泡新菜”:SOD MVVM框架,让WinForms焕发新春》
...全文
20829 24 打赏 收藏 转发到动态 举报
写回复
用AI写文章
24 条回复
切换为时间正序
请发表友善的回复…
发表回复
高大王 2020-10-30
  • 打赏
  • 举报
回复
我要详细的看一下
荆楚丨亡魂 2020-08-13
  • 打赏
  • 举报
回复
学习一下MVVM,支持。
bluedoctor 2020-05-09
  • 打赏
  • 举报
回复
引用 26 楼 zx6630542 的回复:
发功能不发代码的都是老流氓

功能和代码介绍都写到我的新书里面去了,详情见:《致敬平凡的程序员--《SOD框架“企业级”应用数据架构实战》
小灯塔 2020-04-20
  • 打赏
  • 举报
回复


引用 26 楼 zx6630542 的回复:
发功能不发代码的都是老流氓

就是,非常赞同
gaoen10 2020-03-27
  • 打赏
  • 举报
回复
11111111
zx6630542 2017-12-12
  • 打赏
  • 举报
回复
发功能不发代码的都是老流氓
  • 打赏
  • 举报
回复
引用 12 楼 daixf_csdn 的回复:
本质也并不高大上,贴几段分享给大家: UI层数据列表:
/// <summary>
        /// 窗体载入
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected override void Form_Load(object sender, EventArgs e)
        {
            //设置行高以显示图片
            gridViewData.RowHeight = 60;
            imageZoom = new CCImageZoomcs(this, gcData);

            base.Form_Load(sender, e);

            //创建查询条件服务
            QueryService = new EditFormService(goodsQuery, this, dxErrorProvider);
            QueryService.GridLookUpEditBaseDataRegister += metaData.GetGridLookUpEditBaseMetaData;
            QueryService.BindAllColumnsAndEditors();
            QueryService.BindEnterQueryColumns(new KeyEventHandler((sa, ea) => { if (ea.KeyCode == Keys.Enter) bItemQuery.PerformClick(); }));

            //创建列服务
            GridService = new GridControlService(gcData, goodsList);
            GridService.GridLookUpEditBaseDataRegister += metaData.GetGridLookUpEditBaseMetaData;
            GridService.RegisterAllEditors();
            GridService.SetCustomColumnSequence();
            GridService.SetCustomColumnWidth();

            //默认查询
            //数据量大,不自动检索
        }

/// <summary>
        /// BA_Goods 货品信息查询
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected async override void bItemQuery_ItemClick(object sender, DevExpress.XtraBars.ItemClickEventArgs e)
        {
            base.bItemQuery_ItemClick(sender, e);
            if (e.IsCanceled()) return;
            
            var list = await Client.PostAsync<List<ba_goodsViewModel>, ba_goodsViewQueryModel>("api/Goods/GetGoodses", goodsQuery);
            goodsList = new BindingList<ba_goodsViewModel>(list);

            gcData.DataSource = goodsList;

            PictureService.DownLoadGoodsListMainImage(gcData, gridViewData, goodsList);
        }
/// <summary>
        /// BA_Goods 货品信息编辑
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected override void bItemEdit_ItemClick(object sender, DevExpress.XtraBars.ItemClickEventArgs e)
        {
            base.bItemEdit_ItemClick(sender, e);
            if (e.IsCanceled()) return;

            var rowData = gridViewData.GetRow(gridViewData.FocusedRowHandle);
            FormGoodsEdit edit = new FormGoodsEdit(rowData, goodsList.OfType<object>().ToList(), metaData, goodsQuery);
            edit.Saved += edit_Saved;
            edit.ShowDialog();
        }
UI层编辑页面:
/// <summary>
        /// 窗体载入
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected override void Form_Load(object sender, EventArgs e)
        {
            base.Form_Load(sender, e);

            imageZoom = new CCImageZoomcs(this, peGoodsImage, DevExpress.Utils.Win.PopupToolWindowAnchor.TopLeft);

            //创建列属性服务
            EditService = new EditFormService(editModel, parentModelList, this, dxErrorProvider, this.editIndex);
            DynamicMetaDataCache metaData = this.metaData as DynamicMetaDataCache;
            EditService.GridLookUpEditBaseDataRegister += metaData.GetGridLookUpEditBaseMetaData;
            EditService.BindAllColumnsAndEditors();

            //设置控件属性
            if (editMode == DataEditWinMode.Edit)
            {
                teGoodsCode.ReadOnly = true;
                lbSaveAdd.Visibility = DevExpress.XtraBars.BarItemVisibility.Never;
            }
        }
/// <summary>
        /// 保存通用逻辑
        /// </summary>
        /// <param name="e"></param>
        protected async override void OnSave(ResultEventArgs e)
        {
            //保存
            if (editMode == DataEditWinMode.Add || editMode == DataEditWinMode.CopyAdd)
            {
                var list = await Client.PostAsync<object, ba_goodsViewModel>("api/Goods/AddGoods", (ba_goodsViewModel)editModel);
            }
            else
            {
                var list = await Client.PostAsync<object, ba_goodsViewModel>("api/Goods/UpdateGoods", (ba_goodsViewModel)editModel);
            }

            //将新增行加入parentModelList(此List不是BindingList不会和父窗体的列表数据同步)
            if (e.SaveType == WFSaveType.SaveAdd) parentModelList.Add(this.editModel);
        }
ViewModel:
public partial class ba_goodsViewModel: BaseModel, IGoodsImage
    {  
        ///<summary>
        ///货品编码
        ///</summary>
        [Key, Required, StringLength(20)]
        [Width(120)]
		[Display(Name = "货品编码", Order = 10)]
        public string GoodsCode{ get; set; }

        ///<summary>
        ///货品名称
        ///</summary>
        [Required, StringLength(500)]
		[Display(Name = "货品名称", Order = 20)]
        public string GoodsName{ get; set; }

        ///<summary>
        ///货品规格
        ///</summary>
        [StringLength(60)]
		[Display(Name = "货品规格", Order = 30)]
        public string Standard{ get; set; }

        ///<summary>
        ///源编码
        ///</summary>
        [StringLength(20)]
		[Display(Name = "源编码", Order = 40)]
        public string SourceCode{ get; set; }

        ///<summary>
        ///货品条码:可标识单品唯一性
        ///</summary>
        [StringLength(50)]
		[Display(Name = "货品条码", Order = 50)]
        public string BarCode{ get; set; }

        ///<summary>
        ///货品类目
        ///</summary>
        [StringLength(100)]
		[Display(Name = "货品类目", Order = 60)]
        public string Category{ get; set; }

        ///<summary>
        ///库存单位:来自BA_Unit.UnitName
        ///</summary>
        [Required, StringLength(20)]
		[Display(Name = "库存单位", Order = 70)]
        public string Unit{ get; set; }

        ///<summary>
        ///建议库位
        ///</summary>
        [StringLength(20)]
        [Display(Name = "建议库位", Order = 80)]
        [EditorType(XtraEditorType.SearchLookUpEdit, false), SearchLookUpEdit(typeof(SpaceCodeMetaData))]
        public string SuggestedSpace { get; set; }

        ///<summary>
        ///货品图链接:多图以分号分隔
        ///</summary>
        [StringLength(1000), Width(100)]
		[Display(Name = "货品图链接", Order = 90, AutoGenerateField = false)]
        public string ImageURLs{ get; set; }

        ///<summary>
        ///父级货品
        ///</summary>
        [StringLength(20)]
		[Display(Name = "父级货品", Order = 100)]
        public string ParentCode{ get; set; }

        ///<summary>
        ///属性详情:货品多种属性的文字描述,对于属性类子货品有效
        ///</summary>
        [StringLength(10000)]
		[Display(Name = "属性详情", Order = 110)]
        public string Properties{ get; set; }

        ///<summary>
        ///货品详情
        ///</summary>
        [StringLength(1000)]
		[Display(Name = "货品详情", Order = 120)]
        public string Description{ get; set; }

        ///<summary>
        ///状态{1 有效 / 0 无效}
        ///</summary>
        [Required, StringLength(20)]
		[Display(Name = "状态", Order = 130)]
		[DefaultValue("1")]
        [EditorType(XtraEditorType.CheckEdit), CheckEdit(typeof(EnumValidStatus))]
        public string Status{ get; set; }

        ///<summary>
        ///货品图片
        ///</summary>
        [Display(Name = "货品图片", Order = 140)]
        [EditorType(XtraEditorType.PictureEdit), PictureEdit]
        public byte[] GoodsImage { get; set; }

        /// <summary>
        /// 排序视图的列顺序
        /// </summary>
        public override Expression GetViewColumnsSequence()
        {
            //定义UI列排序
            var columnSequence = (new List<ba_goodsViewModel>())
                .AsQueryable()
                .OrderBy(c => new { c.GoodsImage, c.GoodsCode, c.GoodsName, c.Standard, c.SourceCode, c.SuggestedSpace, c.BarCode, c.Category, c.Unit, c.ParentCode, c.Properties, c.Description, c.Status })
                .Expression;

            return columnSequence;
        }
    }
MVC架构么 。这么复杂
bluedoctor 2016-12-19
  • 打赏
  • 举报
回复
引用 22 楼 xiaolinyouni 的回复:
哥,跑csdn分享博客园的链接。。。
那怎么办啊?重新再这里发一遍?
苦苦的潜行者 2016-12-02
  • 打赏
  • 举报
回复
哥,跑csdn分享博客园的链接。。。
vcminusminus 2016-11-28
  • 打赏
  • 举报
回复
在桌面程序中应用MVC,学习了。
屎涂行者 2016-11-16
  • 打赏
  • 举报
回复
bluedoctor 2016-11-16
  • 打赏
  • 举报
回复
虽然这个框架的示例程序是演示MVVM的,但是附加的干货也不少,比如利用SOD框架做MVVM Model 的功能,code first 自动创建数据库很方便,如下所示:

class LocalDbContext : DbContext
    {
        public LocalDbContext()
            : base("default")
        {
            //local 是连接字符串名字 
        }

        protected override bool CheckAllTableExists()
        {
            //创建用户表 
            CheckTableExists<UserEntity>();
            return true;
        }
    }
bluedoctor 2016-11-15
  • 打赏
  • 举报
回复
引用 5 楼 daixf_csdn 的回复:
支持下。我的项目框架用的WInForm也是类似MvvM模式,双向绑定。 同时列表页面和编辑页面的控件,都可通过ViewModel的特性配置,自动渲染和加载(比如下拉列表数据等)。成为一种配置型UI的应用框架。
不错,可否有详细介绍?
Yongbo.Zhu 2016-11-15
  • 打赏
  • 举报
回复
是该结束拖控件时代了
圣殿骑士18 2016-11-15
  • 打赏
  • 举报
回复
本质也并不高大上,贴几段分享给大家: UI层数据列表:
/// <summary>
        /// 窗体载入
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected override void Form_Load(object sender, EventArgs e)
        {
            //设置行高以显示图片
            gridViewData.RowHeight = 60;
            imageZoom = new CCImageZoomcs(this, gcData);

            base.Form_Load(sender, e);

            //创建查询条件服务
            QueryService = new EditFormService(goodsQuery, this, dxErrorProvider);
            QueryService.GridLookUpEditBaseDataRegister += metaData.GetGridLookUpEditBaseMetaData;
            QueryService.BindAllColumnsAndEditors();
            QueryService.BindEnterQueryColumns(new KeyEventHandler((sa, ea) => { if (ea.KeyCode == Keys.Enter) bItemQuery.PerformClick(); }));

            //创建列服务
            GridService = new GridControlService(gcData, goodsList);
            GridService.GridLookUpEditBaseDataRegister += metaData.GetGridLookUpEditBaseMetaData;
            GridService.RegisterAllEditors();
            GridService.SetCustomColumnSequence();
            GridService.SetCustomColumnWidth();

            //默认查询
            //数据量大,不自动检索
        }

/// <summary>
        /// BA_Goods 货品信息查询
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected async override void bItemQuery_ItemClick(object sender, DevExpress.XtraBars.ItemClickEventArgs e)
        {
            base.bItemQuery_ItemClick(sender, e);
            if (e.IsCanceled()) return;
            
            var list = await Client.PostAsync<List<ba_goodsViewModel>, ba_goodsViewQueryModel>("api/Goods/GetGoodses", goodsQuery);
            goodsList = new BindingList<ba_goodsViewModel>(list);

            gcData.DataSource = goodsList;

            PictureService.DownLoadGoodsListMainImage(gcData, gridViewData, goodsList);
        }
/// <summary>
        /// BA_Goods 货品信息编辑
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected override void bItemEdit_ItemClick(object sender, DevExpress.XtraBars.ItemClickEventArgs e)
        {
            base.bItemEdit_ItemClick(sender, e);
            if (e.IsCanceled()) return;

            var rowData = gridViewData.GetRow(gridViewData.FocusedRowHandle);
            FormGoodsEdit edit = new FormGoodsEdit(rowData, goodsList.OfType<object>().ToList(), metaData, goodsQuery);
            edit.Saved += edit_Saved;
            edit.ShowDialog();
        }
UI层编辑页面:
/// <summary>
        /// 窗体载入
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected override void Form_Load(object sender, EventArgs e)
        {
            base.Form_Load(sender, e);

            imageZoom = new CCImageZoomcs(this, peGoodsImage, DevExpress.Utils.Win.PopupToolWindowAnchor.TopLeft);

            //创建列属性服务
            EditService = new EditFormService(editModel, parentModelList, this, dxErrorProvider, this.editIndex);
            DynamicMetaDataCache metaData = this.metaData as DynamicMetaDataCache;
            EditService.GridLookUpEditBaseDataRegister += metaData.GetGridLookUpEditBaseMetaData;
            EditService.BindAllColumnsAndEditors();

            //设置控件属性
            if (editMode == DataEditWinMode.Edit)
            {
                teGoodsCode.ReadOnly = true;
                lbSaveAdd.Visibility = DevExpress.XtraBars.BarItemVisibility.Never;
            }
        }
/// <summary>
        /// 保存通用逻辑
        /// </summary>
        /// <param name="e"></param>
        protected async override void OnSave(ResultEventArgs e)
        {
            //保存
            if (editMode == DataEditWinMode.Add || editMode == DataEditWinMode.CopyAdd)
            {
                var list = await Client.PostAsync<object, ba_goodsViewModel>("api/Goods/AddGoods", (ba_goodsViewModel)editModel);
            }
            else
            {
                var list = await Client.PostAsync<object, ba_goodsViewModel>("api/Goods/UpdateGoods", (ba_goodsViewModel)editModel);
            }

            //将新增行加入parentModelList(此List不是BindingList不会和父窗体的列表数据同步)
            if (e.SaveType == WFSaveType.SaveAdd) parentModelList.Add(this.editModel);
        }
ViewModel:
public partial class ba_goodsViewModel: BaseModel, IGoodsImage
    {  
        ///<summary>
        ///货品编码
        ///</summary>
        [Key, Required, StringLength(20)]
        [Width(120)]
		[Display(Name = "货品编码", Order = 10)]
        public string GoodsCode{ get; set; }

        ///<summary>
        ///货品名称
        ///</summary>
        [Required, StringLength(500)]
		[Display(Name = "货品名称", Order = 20)]
        public string GoodsName{ get; set; }

        ///<summary>
        ///货品规格
        ///</summary>
        [StringLength(60)]
		[Display(Name = "货品规格", Order = 30)]
        public string Standard{ get; set; }

        ///<summary>
        ///源编码
        ///</summary>
        [StringLength(20)]
		[Display(Name = "源编码", Order = 40)]
        public string SourceCode{ get; set; }

        ///<summary>
        ///货品条码:可标识单品唯一性
        ///</summary>
        [StringLength(50)]
		[Display(Name = "货品条码", Order = 50)]
        public string BarCode{ get; set; }

        ///<summary>
        ///货品类目
        ///</summary>
        [StringLength(100)]
		[Display(Name = "货品类目", Order = 60)]
        public string Category{ get; set; }

        ///<summary>
        ///库存单位:来自BA_Unit.UnitName
        ///</summary>
        [Required, StringLength(20)]
		[Display(Name = "库存单位", Order = 70)]
        public string Unit{ get; set; }

        ///<summary>
        ///建议库位
        ///</summary>
        [StringLength(20)]
        [Display(Name = "建议库位", Order = 80)]
        [EditorType(XtraEditorType.SearchLookUpEdit, false), SearchLookUpEdit(typeof(SpaceCodeMetaData))]
        public string SuggestedSpace { get; set; }

        ///<summary>
        ///货品图链接:多图以分号分隔
        ///</summary>
        [StringLength(1000), Width(100)]
		[Display(Name = "货品图链接", Order = 90, AutoGenerateField = false)]
        public string ImageURLs{ get; set; }

        ///<summary>
        ///父级货品
        ///</summary>
        [StringLength(20)]
		[Display(Name = "父级货品", Order = 100)]
        public string ParentCode{ get; set; }

        ///<summary>
        ///属性详情:货品多种属性的文字描述,对于属性类子货品有效
        ///</summary>
        [StringLength(10000)]
		[Display(Name = "属性详情", Order = 110)]
        public string Properties{ get; set; }

        ///<summary>
        ///货品详情
        ///</summary>
        [StringLength(1000)]
		[Display(Name = "货品详情", Order = 120)]
        public string Description{ get; set; }

        ///<summary>
        ///状态{1 有效 / 0 无效}
        ///</summary>
        [Required, StringLength(20)]
		[Display(Name = "状态", Order = 130)]
		[DefaultValue("1")]
        [EditorType(XtraEditorType.CheckEdit), CheckEdit(typeof(EnumValidStatus))]
        public string Status{ get; set; }

        ///<summary>
        ///货品图片
        ///</summary>
        [Display(Name = "货品图片", Order = 140)]
        [EditorType(XtraEditorType.PictureEdit), PictureEdit]
        public byte[] GoodsImage { get; set; }

        /// <summary>
        /// 排序视图的列顺序
        /// </summary>
        public override Expression GetViewColumnsSequence()
        {
            //定义UI列排序
            var columnSequence = (new List<ba_goodsViewModel>())
                .AsQueryable()
                .OrderBy(c => new { c.GoodsImage, c.GoodsCode, c.GoodsName, c.Standard, c.SourceCode, c.SuggestedSpace, c.BarCode, c.Category, c.Unit, c.ParentCode, c.Properties, c.Description, c.Status })
                .Expression;

            return columnSequence;
        }
    }
圣殿骑士18 2016-11-15
  • 打赏
  • 举报
回复
引用 6 楼 bluedoctor 的回复:
[quote=引用 5 楼 daixf_csdn 的回复:] 支持下。我的项目框架用的WInForm也是类似MvvM模式,双向绑定。 同时列表页面和编辑页面的控件,都可通过ViewModel的特性配置,自动渲染和加载(比如下拉列表数据等)。成为一种配置型UI的应用框架。
不错,可否有详细介绍?[/quote] 呵呵,没有,写文档很花时间的,没这个时间。我做框架只用于解决项目需求,不太考虑通用化问题。
bluedoctor 2016-11-15
  • 打赏
  • 举报
回复
多谢版主推荐
足球中国 2016-11-15
  • 打赏
  • 举报
回复
居然会用绑定。
正怒月神 2016-11-15
  • 打赏
  • 举报
回复
已推荐
圣殿骑士18 2016-11-14
  • 打赏
  • 举报
回复
支持下。我的项目框架用的WInForm也是类似MvvM模式,双向绑定。 同时列表页面和编辑页面的控件,都可通过ViewModel的特性配置,自动渲染和加载(比如下拉列表数据等)。成为一种配置型UI的应用框架。
加载更多回复(4)

13,347

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 .NET技术前瞻
社区管理员
  • .NET技术前瞻社区
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧