使用 Pfz.Databasing 框架开发 WinForms 控件






4.25/5 (6投票s)
一个非常易于使用的框架,能够为每种数据类型动态生成正确的控件。
历史
我最近介绍了 Pfz.Databasing 框架。虽然第一次介绍效果不佳,但我认为这个基于它的新框架将展示它的用处。这个框架可以处理序列化文件,从而使测试项目更加容易,并且进行了一些改进,但这并不是真正要展示的内容。
引言
我在另一篇文章中已经说过,但我必须再说一遍:我的想法是创建数据库接口,并让一切都为我完成。如果可能,数据库、窗体,甚至 Web 窗体都应该在不进行编码的情况下完成。好吧,Pfz.Databasing 负责创建数据库,并使搜索和更新变得非常容易。Pfz.Databasing.Controls 试图使创建 Windows 窗体变得非常容易。
如何做到?
好吧,原理很简单,就是创建数据库并创建数据绑定控件。属性会被发现。找到正确的 DataTypeConverter 将值在数据库之间进行转换,并找到正确的 Editor 来显示和编辑值。
但是自定义创建的类型会怎样?嗯,DataTypeConverters 和 Editors 都会被注册。因此,如果您创建了一种新的自定义数据类型,您可以创建新的自定义数据类型转换器来读写数据库,并创建合适的编辑器来显示和编辑值。起初,这可能会让您觉得,“哇!为了这点东西做这么多工作”,但过一段时间,这个想法就会听起来很棒。
我们来比较一下。Delphi 的一个巨大优势是您可以非常容易地创建一个基本的数据库窗体。您拖放字段,就会为您创建一个基本的窗体。
实际上,该框架并没有针对所有字段的“拖放”功能,但您可以将一个代表完整记录的控件拖放到窗体上,设置记录类型,然后所有属性都会显示出来。或者,您可以拖放一个“PropertyBoundControl
”,设置记录类型和属性,就会显示正确的内容。
那么,这个框架真正新颖之处是什么?它是动态的。与其将一个 TextBox
放在窗体上并让它永远成为 TextBox
,不如放置一个绑定到 MyRecord.MyProperty
的控件,而 MyProperty
是一个 int
。如果我为 int
创建了一个新的编辑器,并且不需要重新编辑窗体来放置新编辑器,那么就会使用正确的编辑器。看看在大型系统中这有多么有用?此外,如果您将字段的数据类型更改为更具体但仍与数据库兼容的类型,则会显示该类型的编辑器,而无需查找这些属性在哪里被引用。现在看起来是不是更好了?而且,最后,如果您创建某种“皮肤”,创建多个控件来显示数据,并允许用户选择使用哪一个,您只需要注册正确的控件,整个系统就会为用户使用新控件。
控件
PropertyBoundControl
- 您唯一需要的“数据库控件”。它只显示一个属性(如果您愿意,也可以说是数据库字段)。如果允许,它们也知道如何显示自己的 DisplayNames。RecordBoundControl
- 这个控件显示整个记录,内部使用多个PropertyBoundControl
。它还允许您设置您想显示的“列”以及它们的最小宽度,允许其中许多在一个单行中显示。这使得创建简单布局非常容易。RecordBoundGrid
- 这个控件能够一次显示和编辑多个记录。它创建了许多PropertyBoundControl
的行,并且还以CachedUpdates
的方式工作。
此外,为了帮助快速创建窗体,还有一个 FormEditRecord
,它一次编辑一个记录,还有一个 FormEditRecords
,它已经有一个网格以及添加/删除记录和应用/回滚更改的按钮。您只需要设置记录或记录,一切就绪。
那是简单部分。该框架已包含用于编辑字符串、数字、枚举(显示为组合框)、带日历的日期/时间以及带复选框的布尔值(如果可为空,则有三种状态)的控件,因此创建基本窗体只是使用这些类型的问题。但是,对于特殊类型,有一个更复杂的部分。
IPropertyBoundControlGenerator、PropertyBoundControlGenerator 和 IPropertyBoundControl
为了让您自己的编辑器工作,您需要创建一个控件生成器并将其注册到 PropertyBoundControlGenerator
静态类中。此外,显而易见的是,ControlGenerator
将创建一个控件,该控件必须是 Control
并且也是 IPropertyBoundControl
。
IPropertyBoundControlGenerator
究竟做了什么?它必须告知它可以为哪种属性生成控件,告知它是否可以为这些类型的子类生成编辑器(如果它们可以继承的话,当然),最后,负责创建控件。
例如
using System;
using System.Collections.ObjectModel;
using System.Reflection;
namespace Pfz.Databasing.Controls.Generators
{
internal sealed class StringControlGenerator:
IPropertyBoundControlGenerator
{
private static readonly ReadOnlyCollection<Type> fForTypes = new ReadOnlyCollection<Type>
(
new Type[]
{
typeof(string)
}
);
public ReadOnlyCollection<Type> ForTypes
{
get
{
return fForTypes;
}
}
public bool CanGenerateForSubTypes
{
get
{
return false;
}
}
public IPropertyBoundControl Generate(Type recordType,
PropertyInfo propertyInfo, string displayName)
{
IPropertyBoundControl result = new TextBoxBoundControl(propertyInfo);
if (displayName != null)
return new LabellerControl(result, displayName);
return result;
}
}
}
前面的类仅为字符串生成编辑器。不为其子类生成编辑器(因为它们不存在)。此外,在生成控件时,由于控件本身不显示其 DisplayName,因此会为此创建一个 LabellerControl
,它会在控件的顶部/左侧添加 DisplayName。这个生成器是不是很简单?
好了,现在让我们来理解控件本身
using System.Reflection;
using System.Windows.Forms;
using System.Drawing;
using System.ComponentModel;
namespace Pfz.Databasing.Controls.Generators
{
internal sealed class TextBoxBoundControl:
TextBox,
IPropertyBoundControl
{
internal TextBoxBoundControl(PropertyInfo propertyInfo)
{
RecordProperty = propertyInfo;
ReadOnly = true;
}
public PropertyInfo RecordProperty { get; private set; }
private IRecord fRecord;
public IRecord Record
{
get
{
return fRecord;
}
set
{
fRecord = value;
if (value == null)
{
ReadOnly = true;
return;
}
ReadRecord();
ReadOnly = value.GetRecordMode() == RecordMode.ReadOnly;
}
}
public void ReadRecord()
{
Text = (string)RecordProperty.GetValue(Record, null);
}
public void WriteRecord()
{
string text = Text;
if (text == "")
text = null;
RecordProperty.SetValue(Record, text, null);
}
string IPropertyBoundControl.DisplayName
{
get
{
return null;
}
}
protected override void OnValidating(CancelEventArgs e)
{
if (!ReadOnly)
WriteRecord();
base.OnValidating(e);
}
}
}
控件继承自 TextBox
,因为 TextBox
已经能够编辑字符串。RecordProperty
被存储起来,因为它是接口的一部分,并且控件一开始是只读的,因为当它被创建且未绑定时,它必须是只读的。获取记录很简单,但设置它时还必须更新 ReadOnly 并调用 ReadRecord
来刷新正在显示的值。ReadRecord
和 WriteRecord
,嗯,将文本设置为属性的值,并将属性的值设置为文本,只将空字符串转换为 null,因为空字符串表示 null。DisplayName 由 LabellerControl
获取/设置,所以我们可以抛出异常或返回 null
,我们实际上不需要实现它。而且,为了在焦点丢失后立即更新记录,我们实现了 OnValidating
来调用 WriteRecord
。
这差不多就是全部了。控件已经准备好使用了。我们只需要**一件事**:注册 ControlGenerator
。为此,我们调用
PropertyBoundControlGenerator.Add(new StringControlGenerator());
当然,这个编辑器已经存在了,所以您不需要这样做。
未来
当然,我计划创建更多控件,修复 bug,尤其是让网格运行得更快,因为今天,它在应用记录时总是重新创建所有控件,但我的下一步是创建一个 Web 版本框架,这样接口和业务对象就可以重用,并且窗体的创建在 Web 上可以像在桌面一样简单。我甚至计划创建一个窗体编辑器和一个“窗体读取器”用于 Web 和桌面,这样您就可以创建一个可以在两者之间无任何更改运行的窗体,但这属于未来。
我希望这个框架对您和我一样有用。