架构指南:Windows Forms、泛型、Auto-Mapper、Entity Framework、框架设计等等...
基于框架构建Windows Forms系统。这将帮助您更快地开发基于窗体的应用程序。
数据库在其中 - DemoCustomerFrms\DB\Raptor_Core_Local_DB_Backup_17_11_2010。请查看 - Rocket Framework (.NET 4.0) 了解如何在实践中使用它。
目录
- 引言
- 背景
- 如何使用框架?
- 让我们看看我们的演示框架
- 我们如何逐步构建框架?
- 高层设计概述
- 让我们看一下“Demo.Framework”模块设计
- 看看下面的“BaseGridView”代码
- 让我们看一下“Demo.Data”模块设计
- 让我们看一下“Demo.Core”模块设计
- 让我们看一下“Demo.Core.UserControls”
- 数据控件的模块设计实现
- 最后说明
- 历史
引言
Web系统设计有许多在线资料支持。相比之下,用于Windows-Forms(桌面应用程序)系统设计的支持材料较少。在20世纪80年代,在两层系统的时代,桌面应用程序统治着软件世界。现在时代变了,软件极客们现在专注于Web系统,让开发人员随意设计桌面应用程序。如今,您几乎看不到任何架构原则被应用于桌面应用程序。因此,我考虑为这个被忽视的领域提供一些见解,以改进桌面应用程序的设计方面。
在此尝试中,我将演示一个简单的桌面应用程序的架构。此外,我还将开发一个可重用的框架,您可以将其用于开发任何桌面应用程序。
当我谈论“框架”时,您可能想知道框架到底是什么。我认为围绕“框架”这个词有很多误解。所以我也将为您介绍“框架”。我将在本文的早期进行介绍。但在此之前,让我先告诉您撰写本文的想法是如何开始的。
背景
我们公司目前正在使用Visual Studio 2010开发一个大型的基于Windows Forms的.NET Framework 4.0应用程序。其中,我们需要开发一个行政用户界面(用于执行主记录的CRUD操作),具有某些权限的用户可以直接编辑数据库表的某些元素。所以我们需要一套屏幕,他们可以在其中编辑DB表的记录。下面显示了一个这样的屏幕供您参考。您可以使用我们的“框架”在20分钟内开发此屏幕。
在该项目初期,我像往常一样花了几天时间研究,寻找合适的技术和设计方法的组合来形成该系统的设计。我考虑开发一个通用的框架,其主要目标是支持编辑DB记录。该框架的计划方式是,我们可以将其用作任何“桌面应用程序”开发的基础。撰写本文的主要目的是将该“框架”介绍给CodeProject社区。
我认为系统架构不仅应该支持直接编辑数据库表,还应该支持处理包含来自不同表的多个字段的自定义业务对象。开发一个支持此功能的通用“框架”几乎是不可能的,因为不同的领域有不同的业务需求。因此,我认为在通用框架之上引入多个“包装”层以支持处理特定领域的需求非常重要。换句话说,这意味着我们需要一个特定于领域的框架,该框架建立在通用框架之上。这是为了实现系统的独特领域特定功能。因此,未来将有几个特定于领域的框架添加到此库中。此外,为了满足客户非常特殊的、高度定制的独特需求,我计划在通用框架和特定领域框架之上再增加一个层。
如果一切顺利,一旦这个“架构”稳定下来,当有新客户出现时,我们只需从我们的软件商店复制通用框架和相应的特定领域框架,然后花几天时间为其添加客户特定功能,然后将所有内容打包形成发布版的Beta版本。这听起来令人兴奋,不是吗?
如何使用框架?

让我们看一下上面的图片。这是一个框架,您可以在其中轻松地形成一个房间。然后,只需做更多工作,就可以将其扩展为一个房间的集合。然后,您可以开发一些扩展,形成“汽车旅馆”、“酒店”或豪华公寓。这就是您如何使用框架来更快地构建事物。我们也将使用这个完全相同的概念来构建我们的软件系统。
这种基于框架构建的概念并不新鲜。即使您使用的语言.NET/C#,也是一个框架。不仅如此,.NET还有建立在旧框架之上的新框架。在文章的下一部分,您将看到.NET Framework 3.0是如何建立在.NET Framework 2.0之上的。
让我们看看我们的演示框架
这张图显示了Microsoft是如何在.NET Framework 2.0之上构建.NET Framework 3.0的。这表明了如何使用一个框架来在其之上构建另一个框架。
正如上面图片所示,我们将把我们的框架建立在.NET Framework 4.0之上。这是.NET Framework家族的最新版本。
我们如何逐步构建框架?
这表明了一个策略,您可以使用该策略来开发一个“框架”,而无需在初期花费太多时间。首先,您需要有勇气对您周围的人想要添加到“框架”中的不断增长的“万一”类需求说“不”。您需要定义框架的边界。然后,您需要制定一些策略,以便您可以高效地构建此系统。
正如您在图中看到的,您必须为主要框架确定一套核心功能。然后让特定领域的框架通过每一次自定义实现自动构建。
高层设计概述
这是我们系统的包图。我使用了一些颜色编码,以便您更容易理解。
正如您所看到的,有几个模块被用于开发这个系统。您可以看到最顶部的通用“Demo.Framework”。然后,通过使用它,形成了核心业务逻辑以及用户控件库。“Demo.Common”对所有人都通用,是我们将在多个(所有)包中共享的。数据访问层(Demo.Data)库是使用Microsoft ADO.NET Entity Framework开发的。
让我们看一下“Demo.Framework”模块设计
说得够多了,现在让我们直接进入主题。别害怕,我知道有些人天生就害怕系统架构。我对此也曾感到“化学”。我确信我一开始就没有走对路。在软件世界里,我也见过过度设计的系统,有着永无止境的类层次结构。其中一些系统对于它们的需求来说过于复杂。但在我们的设计中,我保持了非常简单。我希望每个人都能理解、维护,并轻松地升级系统。
这个设计包含一些但有价值的“接口”。不要认为这些接口让设计看起来复杂或优雅,我添加它们是有充分理由的,也是为了实现一些否则无法实现的事情。我将在本文后面解释这些设计决策。
这里使用了七个类接口。您可能听说过,通常“接口”将对象的定义与其实现分开。如果您将接口视为合同,那么很清楚合同的双方都有角色要扮演。接口的发布者(或等式中的框架方)同意永远不更改该接口,而实现者(或等式中的自定义实现方)同意完全按照设计来实现接口。这有助于在不了解其实现的情况下,将接口用于许多其他泛型类。
-
IHandler<V>
- 这是用于实现所有处理程序类的泛型接口。类型为“Handler”的类是用于实现系统中所有业务对象处理的类。 -
IMapper<V, D>
- 这包装了一个第三方库,称为Auto-Mapper。Auto-Mapper(参考:http://automapper.codeplex.com/)使用基于约定的匹配算法来匹配源对象到其目标对象的值。 -
IObject
– 这个接口实现了抽象的BaseObject
,它用作所有业务对象的基础类。 -
IDataContainer<V>
- 这是用于实现BaseData<V>
,即泛型用户控件的。我必须告诉您,Microsoft 尚未准备好支持将面向对象编程概念应用于用户控件或开发泛型用户控件。IDE 在看到“<”符号后立即会给您带来麻烦。泛型实现的优雅让我忽略了这种麻烦。当您按照我的指示进行操作时,您也将不得不面对 Visual Studio IDE 带来的麻烦(我注意到这个bug大约在2003年就被报告了,但在VS 2010中仍然存在)。 -
IGridView
– 这是用于实现泛型BaseGridView<H, V>
的接口。其中H
是IHandler<V>
类型,而V
是BaseObject
类型。
注意:V
代表视图或业务对象,而D
代表数据对象或数据库表。
这个框架设计得很优雅,复杂性最低。我知道有些人可能仍然觉得难以理解。如果您没有处理泛型、接口等方面的专业知识,那么只需找到使用此框架的模式并开始使用它,然后您会慢慢理解它。即使是今天,也有许多开发人员利用这一点来构建他们的应用程序,而对框架的设计或泛型或接口一无所知。一旦理解了,设计模式本身就会指导您如何使用它。
看看下面的“BaseGridView”代码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace Demo.Framework
{
public abstract partial class BaseGridView<H, V> : BaseControl, IGridView
where H : IHandler<V>
where V: BaseObject
{
public event DataGridViewCellEventHandler DataRowSelected;
public BaseGridView(H h)
{
InitializeComponent();
_hadler = h;
}
private H _hadler;
public virtual void LoadData()
{
this.DataList = _hadler.GetAll();
}
private V _selectedItem;
public V SelectedItem
{
get { return _selectedItem; }
}
private List<V> _originalList = null;
private List<V> _dataList;
public List<V> DataList
{
get { return _dataList; }
set
{
_dataList = value;
this.dataGridViewMain.DataSource = null;
if (_dataList == null) return;
this.dataGridViewMain.Columns.Clear();
this.dataGridViewMain.AutoGenerateColumns = true;
this.dataGridViewMain.DataSource = this._dataList;
OnHideColumns(ref dataGridViewMain);
if (_dataList.Count > 0)
{
int cCount = this.dataGridViewMain.Columns.Count;
foreach (KeyValuePair<string, object> item in _dataList[0].Data)
{
DataGridViewTextBoxColumn mRef = new DataGridViewTextBoxColumn();
mRef.HeaderText = item.Key;
this.dataGridViewMain.Columns.Add(mRef);
}
for (int i = 0; i < dataGridViewMain.RowCount; i++)
{
int c = cCount;
IObject boundItem = (V)dataGridViewMain.Rows[i].DataBoundItem;
foreach (KeyValuePair<string, object> item in boundItem.Data)
this.dataGridViewMain[c++, i].Value = item.Value;
}
}
}
}
protected virtual void OnHideColumns(ref DataGridView dataGridView)
{
dataGridView.Columns["Id"].Visible = false;
dataGridView.Columns["Data"].Visible = false;
dataGridView.Columns["SourcePoco"].Visible = false;
}
public string Heading
{
get { return groupBoxMain.Text; }
set { groupBoxMain.Text = value; }
}
protected List<V> Search(string query)
{
List<V> list = new List<V>();
string[] qItems = query.Split(' ');
foreach (V item in _dataList)
{
for (int i = 0; i < qItems.Length; i++)
if (item.FoundIt(qItems[i]))
list.Add(item);
}
return (list.Count > 0) ? list : null;
}
private void buttonSearch_Click(object sender, EventArgs e)
{
if (_originalList == null)
{
_originalList = _dataList;
return;
}
string s = this.textBoxSearch.Text.Trim();
this.dataGridViewMain.Columns.Clear();
if (string.IsNullOrEmpty(s))
this.DataList = _originalList;
else
this.DataList = this.Search(s);
}
private void dataGridViewMain_RowEnter(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex >= 0)
_selectedItem = (V)dataGridViewMain.Rows[e.RowIndex].DataBoundItem;
if (DataRowSelected != null)
DataRowSelected(_selectedItem, e);
}
private void buttonRefresh_Click(object sender, EventArgs e)
{
LoadData();
this.textBoxSearch.Text = "";
}
}
}
我希望您关注这个名为BaseGridView<H>
的泛型抽象类的顶部。在实现中,构造函数用于获取传递给它的处理程序的实现版本(H
)。由于我们系统中所有处理程序都实现IHandler
,因此系统可以通过IHandler
接口利用处理程序的内置方法。有趣的是,即使不知道实际实现,我们也可以做到这一点。这是我们代码中一个非常有趣的部分。随着您阅读本文,您将能更好地理解这种设计方法。同时,我邀请您仔细检查源代码。
BaseHandler<V, D>
- 这个抽象类实现了IHandler
和IMapper
接口。它包含业务对象的处理的通用实现。所有这些类和接口都使用两个泛型,即类型为BaseObject
和EntityObject
的V
和D
。BaseHandler
也与IRepository<D>
接口关联。该关联用于隐藏存储库实现。在实现特定处理程序时传递给它的存储库对象用于从数据库检索与特定数据对象相关的数据。那里使用Auto-Mapper将它们从EntityObject
类型的D
映射到BaseObject
类型的V
,然后传递给下一层。IRepository<D>
的实现是在Demo.Data库下完成的。我稍后会解释那部分。
基类帮助我们一次性完成通用实现,并可与多种类型重用。
让我们看一下“Demo.Data”模块设计
该系统使用ADO.NET Entity Framework(.NET 4)来生成数据访问层(参考:http://msdn.microsoft.com/en-us/data/ee712907.aspx)。我还继续使用T4模板,但后来发现这里介绍的特殊泛型存储库实现用T4模板不易实现。所以我只使用Entity Framework生成的默认代码。为了让您理解,请看下面的泛型存储库代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Objects;
using System.Linq.Expressions;
using System.Data;
using System.Data.Objects.DataClasses;
using System.Data.Common;
using Demo.Framework;
namespace Demo.Data
{
public sealed class DemoRepository<T> : IRepository<T>
where T : EntityObject
{
static readonly DemoRepository<T> instance
= new DemoRepository<T>(new DemoCoreEntities());
private static readonly Object _lockObject = new Object();
// Explicit static constructor to tell C# compiler
// not to mark type as beforefieldinit
static DemoRepository()
{
if (_lockObject == null)
_lockObject = new Object();
}
public static DemoRepository<T> Instance
{
get
{
return instance;
}
}
DemoRepository(ObjectContext repositoryContext)
{
_repositoryContext = repositoryContext ?? new DemoCoreEntities();
_objectSet = _repositoryContext.CreateObjectSet<T>();
}
private static ObjectContext _repositoryContext;
private ObjectSet<T> _objectSet;
public ObjectSet<T> ObjectSet
{
get
{
return _objectSet;
}
}
#region IRepository Members
public IRepository<T> GetRepository()
{
return Instance;
}
public void Add(T entity)
{
lock (_lockObject)
{
this._objectSet.AddObject(entity);
_repositoryContext.SaveChanges();
_repositoryContext.AcceptAllChanges();
}
}
public void Update(T entity)
{
lock (_lockObject)
{
_repositoryContext.ApplyOriginalValues(((IEntityWithKey)entity)
.EntityKey.EntitySetName, entity);
_repositoryContext.Refresh(RefreshMode.ClientWins, _objectSet);
_repositoryContext.SaveChanges();
_repositoryContext.AcceptAllChanges();
}
}
public void Delete(T entity)
{
lock (_lockObject)
{
this._objectSet.DeleteObject(entity);
_repositoryContext.Refresh(RefreshMode.ClientWins, _objectSet);
_repositoryContext.SaveChanges();
_repositoryContext.AcceptAllChanges();
}
}
public void DeleteAll()
{
_repositoryContext
.ExecuteStoreCommand("DELETE " + _objectSet.EntitySet.ElementType.Name);
}
public IList<T> GetAll()
{
lock (_lockObject)
{
return this._objectSet.ToList<T>();
}
}
public IList<T> GetAll(Expression<Func<T, bool>> whereCondition)
{
lock (_lockObject)
{
return this._objectSet.Where(whereCondition).ToList<T>();
}
}
public T GetSingle(Expression<Func<T, bool>> whereCondition)
{
lock (_lockObject)
{
return this._objectSet.Where(whereCondition).FirstOrDefault<T>();
}
}
public IQueryable<T> GetQueryable()
{
lock (_lockObject)
{
return this._objectSet.AsQueryable<T>();
}
}
public long Count()
{
lock (_lockObject)
{
return this._objectSet.LongCount<T>();
}
}
public long Count(Expression<Func<T, bool>> whereCondition)
{
lock (_lockObject)
{
return this._objectSet.Where(whereCondition).LongCount<T>();
}
}
#endregion
}
}
您上面看到的代码是特殊的。我试图尽可能有效地使用“通用”存储库。正如您所看到的,存储库被实现为一个对象。单例设计模式的线程安全版本用于实现存储库。此外,还通过_lockObject
进行了额外的保护,以允许并发访问存储库。仅仅为了演示直接在存储库中使用SQL命令,DeleteAll()
方法被编写了。
让我们看一下“Demo.Core”模块设计
“Demo.Core”用于实现所有业务对象及其处理函数。按照MVC架构,我将所有业务对象的名称都加上“View”后缀。因此,在该库中,您会找到两种类型的类。一种是处理程序类型(它是MVC架构的Service
类的变体),另一种是View类型。处理程序类型的类包含与特定视图对象相关的所有操作。让我们打开HandlerApplicationUser
类,看看里面有什么。
HandlerApplicationView
类如下。它几乎是一个空类。一个要求是向其基类传递正确的存储库实例。此外,如果您想进行从“数据对象”到“业务对象”或反之的特殊映射,那么您需要使用ForwardMap
或BackwardMap
方法来更新映射表达式。
例如,对于BackwarMap
:如果您在一个表中包含“FirstName”、“Initials”和“LastName”,那么相应的数据对象将有三个同名的字段。如果您想将数据对象的这三个字段映射到业务对象中一个名为Name
的新字段,那么您可以使用提供的BackwardMap
映射表达式。正如您所看到的,BackwardMap
方法具有IMappingExpression<ApplicationUser, ApplicationUserView>
,在本例中,它包含将ApplicationUser
映射到ApplicationUserView
的详细信息。
lambda表达式困扰您吗?那一点也不难,您只需要理解模式;一旦理解了,lambda表达式就没有什么难的了。
LogPrefix
属性查找下面的处理程序类,用于log4net。如果您查看框架库Common文件夹中的LoggerBase
类,您将理解“Logger Base”包装了log4net函数,以便灵活地将其替换为另一个。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Demo.Data;
using AutoMapper;
using Demo.Core.Models;
using Demo.Framework;
namespace Demo.Core
{
public class HandlerApplicationUser : BaseHandler<ApplicationUserView, ApplicationUser>
{
public HandlerApplicationUser()
: base(DemoRepository<ApplicationUser>.Instance)
{
}
public override void ForwardMap(IMappingExpression<ApplicationUserView, ApplicationUser> mappingExpression)
{
mappingExpression
.ForMember(dest => dest.ApplicationUserId, opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.Gender, opt => opt.MapFrom(src => (src.SelectedGender.Key == 1) ? true : false))
.ForMember(dest => dest.AdditionalData, opt => opt.MapFrom(src => (src.Data != null) ? src.Data.ToString() : ""));
}
public override void BackwardMap(
AutoMapper.IMappingExpression<ApplicationUser, ApplicationUserView> mappingExpression)
{
mappingExpression
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.ApplicationUserId))
.ForMember(dest => dest.SelectedGender, opt => opt.MapFrom(src =>
(src.Gender) ? new KeyValuePair<int, string>(1, "Male") :
new KeyValuePair<int, string>(2, "Female")))
.ForMember(dest => dest.SourcePoco, opt => opt.MapFrom(src => src))
.ForMember(dest => dest.Data, opt =>
opt.MapFrom(src => AdditionalData.Resolve(src.AdditionalData)));
}
protected override Type LogPrefix
{
get { return this.GetType(); }
}
}
}
让我们看一下“Demo.Core.UserControls”模块设计
这就是我们开发所有用户控件的地方。在那里,正如我之前所说的,我大量使用了泛型。但设计方法与上述模块几乎相同。所以,我将留给您自行查看源代码并理解。
ApplicationUserData
– 这是一个通过扩展BaseData
创建的用户控件。在这里,您将看到文本框、下拉列表和标签,允许查看、编辑业务对象。在创建此类数据控件时,您需要遵循几个步骤。
- 创建业务对象(命名为ApplicationUserView)。
- 创建处理业务对象的处理程序类(命名为HandlerApplicationUser)。
- 通过选择“Object”作为源来添加新的“Data Source”(请看下面的屏幕,您需要在此处单击“New Data Source”)。这将允许您选择可用于生成所需数据绑定控件的对象。您需要先找到正确的类库,然后选择正确的业务对象来创建这个新的“Data Source”(在本例中,我选择了“ApplicationUserView”作为源对象)。
- 添加一个新的用户控件,使用标准的名称,如<业务对象名称>Data.cs(在本例中是“ApplicationUserData”)。
- 将整个视图拖到用户控件上,并删除您不希望用户在用户控件上查看/编辑的那些。
- 导航到新添加的用户控件的源代码,并将父类从“
: UserControl
”更改为“: BaseData<业务对象名称>
”(在本例中是BaseData<ApplicationUserView>
)。此更改将使您覆盖三个方法,如下面“数据控件实现”部分所述。
注意:使用IDE编辑这些用户控件是不可能的,因为Visual Studio无法理解泛型类型的用户控件。如果您想在数据控件构建后进行编辑,则需要注释掉所有三个重写的方法,并将父类改回“UserControl”。这将允许您使用IDE进行控件编辑。一旦编辑完成,您需要将更改回滚到原始状态。
ApplicationUserDataGridView
– 这个用户控件能够列出一组业务对象。它还有一个通用搜索功能。您可以使用DisplayNameAttribute
控制网格列的名称。除此之外,它对于使用者来说没有太多其他功能。您可以查看源代码来理解这里可以实现的各种功能。需要注意的一点是,如果您想在显示某个列之前隐藏它,可以使用方法名OnHideColumns
。
ApplicationUserManager
– 这是主管理器控件,可帮助您完成与业务对象相关的 CRUD 操作。您只需向基类传递一些参数,它就会为您处理其余的事情。
数据控件实现
如果您需要在此库中编写任何代码,那么这些代码仅包含在相应的数据控件中。我下面提供了一个实现。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Demo.Framework;
using Demo.Core.Models;
using Demo.Common;
namespace Demo.Core.Controls.ChildControls
{
public partial class ApplicationUserData : BaseData<ApplicationUserView>
{
public ApplicationUserData()
{
InitializeComponent();
}
public override void SetBindingSource(ref BindingSource DemoBindingSource)
{
DemoBindingSource = this.applicationUserViewBindingSource;
}
public override ApplicationUserView MapIt(int Id)
{
ApplicationUserView apView = this.BindedView;
apView.Id = Id;
apView.UserName = this.userNameTextBox.Text;
apView.Password = this.passwordTextBox.Text;
apView.CustomerId = NullHandler.ConvertToInt(this.customerIdTextBox.Text);
apView.SelectedGender = (KeyValuePair<int, string>)this.genderCombo1.SelectedItem;
apView.FirstName = this.firstNameTextBox.Text;
apView.ShortName = this.shortNameTextBox.Text;
apView.MiddleName = this.middleNameTextBox.Text;
apView.LastName = this.lastNameTextBox.Text;
apView.StatusTypeId = NullHandler.ConvertToInt(this.statusTypeIdTextBox.Text);
apView.LastLoginDateTime = this.lastLoginDateTimeDateTimePicker.Value;
apView.Email = this.emailTextBox.Text;
apView.Phone = this.phoneTextBox.Text;
return apView;
}
protected override void _DemoBindingSource_DataSourceChanged(object sender, EventArgs e)
{
if (this.BindedView != null)
this.genderCombo1.SelectedItem = this.BindedView.SelectedGender;
}
}
}
最后说明
此处提供的演示是“按原样”提供的,不提供任何形式的保证。我不对提供的源代码没有错误或它们将满足您对任何特定应用程序的要求做出任何明示或暗示的保证。由您来评估、学习、审查并按适用性使用它。
历史
- 初始发布 - 2010年11月22日。
- 添加了指向Rocket Framework的链接。