65.9K
CodeProject 正在变化。 阅读更多。
Home

适用于 .NET 的连续表单

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.14/5 (24投票s)

2005年3月21日

8分钟阅读

viewsIcon

135750

downloadIcon

2425

备受追捧的 MS Access 功能现已适用于 .NET。相当于 ASP.NET 的 DataList for WinForms,为富客户端环境提供了一个数据绑定模板控件。包含完整的源代码。

引言

任何在 MS Access 中开发过应用程序的人都知道,RAD 方法和系统是多么的快速开发。不仅适用于原型或简单的两小时项目,甚至适用于功能齐全、多开发人员、多层、企业级的宏伟项目。该平台拥有所有预构建的功能、易于访问的组件和“触手可及”的设计界面,可以为您节省数千个人力时,无需担心 UI 问题。

在 VS 中,微软显然试图为那些不会编程的人提供数据访问。如果是这样,那么,如果比尔·盖茨在支付 Access 设计界面设计师的薪水,为什么他不将他们用于 VS 环境,这超出了我的理解。为什么,我必须筛选属性管理器来更改按钮上文本的颜色或将标签中的文本右对齐,而微软拥有类似 Access 的 RAD 的源代码和设计规范?

够了,关于我希望从微软那里看到什么,现在是我为他们做了什么。

我从 Access 迁移到 C# 时最大的损失就是连续表单!我们都知道它们。也用过它们。当你想一次显示多条记录,但又想布局每条记录时,它们是完美的工具。在 ASPX 中,它一直是“模板”,在 Win forms 中,你找不到类似的东西。所以,这是我的解决方案。

一旦它在 .NET 中创建,它现在就比它在 Access 中时更好,因为它现在是 OOP,并且每个记录面板都是一个真正的实例。更不用说它可以从所有 .NET 语言(如 VB 或 C++ 的托管扩展)中使用。

理解控件

使用反射,我为数据源中的每条记录克隆一个 Panel 及其所有控件。因此,您可以一次滚动浏览数据源中的多条记录。这就像在 DataGrid 中呈现数据,但现在您可以布局每条记录的显示方式。我还触发一个 ItemDataBound 事件,以便您可以为子控件进行自定义数据绑定和上下文格式设置。

所有这些都包含在两个文件中,一个子类化控件、两个实用类、一个委托和一个接口中。总共 **不到 200 行** 代码。

核心类 - 控件

我利用的第一个内置功能是 Panel(终于)能够精确地拉伸其虚拟表面,让用户滚动到任何位置,而程序员只需要担心控件的逻辑位置。这正是您希望能够显示多条记录的控件应有的行为方式。所以,这就是我子类化的控件。

public class NewPanel : System.Windows.Forms.Panel

属性

为了使此代码片段尽可能可重用,并使其易于使用 VS 的图形布局工具,我们假设每条记录将表示为一个 Panel 及其上的控件。所以我添加了一个 public 变量(我知道它应该是一个属性),类型为 Panel,它必须设置为将用作记录模板的 Panel

控件中创建的变量如下所示

public System.Windows.Forms.Panel BasePanel = null;

将在使用项目中的设计面板中像这样分配一个引用

newPanel2.BasePanel = panel1;

Panel1:

此控件的目的是呈现(和编辑)数据,因此,我们必须有一个 DataSource,因此添加了一个类型为 IDataSocketpublic 变量。有关我的数据无关解决方案的完整解释,请参阅本文末尾。在此期间,只需使用提供的套接字之一,例如 RowCollectionSocket,它是内置的,用于与 DataSet 的部分进行接口。

控件中创建的变量如下所示

public IDataSocket DataSource = null;

将在使用项目中的数据容器中像这样分配一个引用

newPanel2.DataSource = new DataRowCollectionSocket(dsAddresses2.Addresses.Rows);

用作模板的 Panel 上的控件应具有到同一数据容器中字段的 DataBindings,该数据容器用作我们 PanelDataSource。所有简单的 DataBindings,用于任何属性,都应该能够工作。

在代码中,这可能看起来像这样

this.comboBox1.DataBindings.Add(new System.Windows.Forms.Binding("Text", 
                                this.dsAddresses2, "Addresses.State"));
this.txtZip.DataBindings.Add(new System.Windows.Forms.Binding("Text", 
                             this.dsAddresses2, "Addresses.Zip"));
this.txtCity.DataBindings.Add(new System.Windows.Forms.Binding("Text", 
                              this.dsAddresses2, "Addresses.City"));

或者在图形上像这样

绑定函数

其余的工作在 Bind() 函数中完成,并在私有函数 DeepCloneControl() 的帮助下完成。

出于某种原因,您无法 foreach 遍历 Controls 集合,所以我将控件的引用复制到一个数组中。然后,我将相同的数组保留下来,用于所有数据行。

Control[] Carr = new Control[BasePanel.Controls.Count];
BasePanel.Controls.CopyTo(Carr,0);

Controls 集合也缺少通过名称获取子项的方法。当您尝试在 ItemDataBound 事件中访问内容时,这可能会造成问题。所以我创建一个 Hashtable 来存储数组中每个控件的序号,以后我可以使用它通过 Controls 集合的索引器来访问任何我想要的控件。这个巧妙的小片段封装在 EventArgs 类的 ControlFromName() 函数中。

地图创建

Hashtable ctMap = new Hashtable();
for(int i =0; i < Carr.Length; i++)
    ctMap.Add(Carr[i].Name,i);

使用地图的函数

public System.Windows.Forms.Control ControlFromName(string name)
{
    try
    {
        return Item.Controls[(int)ControlMap[name]];
    }
    catch(InvalidCastException)
    {
        return null;
    }
    catch(NullReferenceException)
    {
        return null;
    }
}

然后我们得到我们所追求的,循环遍历数据。对于每一行(项),我克隆用作模板的 Panel。我将新的 Panel 添加到滚动面板中。我更改的唯一属性是位置,以便它紧靠在前一个 Panel 的下方(您可能想添加一个属性来设置间距)。我循环遍历控件并克隆每一个。最后,我触发 ItemDataBound 事件,然后继续处理 DataSource 中的下一行(项)。

for(int i = 0; i < DataSource.Count; i++)
{
    Panel tempPanel = (Panel)DeepCloneControl(BasePanel,i);

    tempPanel.Top = i * BasePanel.Height;
    tempPanel.Left = 0;
    panel1.Controls.Add(tempPanel);
    foreach(Control c in Carr)
    {
        tempPanel.Controls.Add(DeepCloneControl(c,i));
    }
    if(ItemDataBound != null)
        ItemDataBound(this, new ContinuousItemEventArgs(tempPanel, 
                                DataSource[i], ctMap));

}

DeepCloneControl 函数

所有克隆非克隆对象所需的反射都可以通过 GetType 函数(以及与类/对象/控件关联的 Type)访问。一旦我有了类型对象,几乎所有工作都通过 MemberInfo 的子类完成。

Type ctrlType = subject.GetType();
ConstructorInfo cInfo = ctrlType.GetConstructor(Type.EmptyTypes);
Control retControl = (Control)cInfo.Invoke(null);

在那时,我只需要做的是创建(通过反射 / ConstructorInfo)相同类型的控件,并将其存储在一个类型为 Control 的变量中(因为我知道它必须继承自它)。下一步是循环遍历每个安全属性,并将值从源(主/模板)控件复制到新创建控件中的相同属性。我确定安全的方法是只获取那些类型为值类型或类型为 string 的属性。尝试复制引用(当然也尝试克隆)任何其他内容都可能带来灾难性的后果。想象一下所有控件都写入同一个图形对象。大多数情况下,您将导致程序崩溃。我可以保留一个安全属性列表,但 **我讨厌特殊情况代码**,我认为它揭示了设计的缺陷,并且只有在没有控制和选择时才使用它们。话虽如此,我有一个第二个循环用于为新控件创建 DataBindings,我检查一个名为 DataSource 的属性,并复制其中指定的容器的引用,以便像组合框这样的控件仍然具有其 RowSource 信息。

foreach(PropertyInfo pInfo in 
       ctrlType.GetProperties(BindingFlags.Public|BindingFlags.Instance))
{
    if (pInfo.CanWrite && 
       (pInfo.PropertyType.IsValueType || 
        pInfo.PropertyType.Name == "String"))
    {
        try
        {
            switch(pInfo.Name)
            {
                case "Parent":
                    break;
                default:
                    pInfo.SetValue(retControl,pInfo.GetValue(subject,null),null);
                    break;
            }
        }
        catch(Exception ex)
        {
            Console.WriteLine("Could not assign the value" + 
               " of {0} to \nObject:\t{1}\nOf Type:\t{2}\nBecause:\t{3}",
               pInfo.Name, subject.Name, ctrlType.Name, ex.Message);
        }
    }
}

DataBindings 是通过从源(主/模板)控件中获取字段名,解析字符串,并将当前行(项)指定为 DataSource 来创建的。

string BindingMember = ctrlBinding.BindingMemberInfo.BindingMember;
string BindingField = BindingMember.Substring(BindingMember.LastIndexOf("."));
retControl.DataBindings.Add(ctrlBinding.PropertyName,
    DataSource[i],
    BindingField);

ContinuousItemDataBound & ContinuousItemEventArgs

这两项结合起来,为您提供了对每行(记录/项)更改的控制。该事件在所有处理结束后,为每一行(记录/项)触发一次。它为您提供了一个机会——在 EventArgs 的帮助下——分析特定的数据项,并根据需要更改呈现该项的 Panel

成员

EventArgs 有三个公共属性和一个函数。属性包含指向为此数据项创建的 Panel、特定数据项以及用于一个函数以使您能够访问 Panel 上任何控件的名称到集合序号的映射的引用。

public System.Windows.Forms.Panel        Item;
public object                            Data;
public System.Collections.Hashtable        ControlMap;

IDataSocket & DataRowCollectionSocket

IDataSocket 是我能够处理任何可以容纳多条数据项的数据源类型的方法。为什么所有这些类不继承自一个公共接口,这超出了我的理解。似乎合乎逻辑的是,每个可以循环遍历的集合、表、流都应该继承自同一个接口,并且该接口应该具有执行此操作所需的成员。然后应该有一个接口,用于任何公开索引器的类。我也不想让我的代码充斥着类型转换,所以我一石二鸟,创建了一个包含计数属性和索引器的接口。现在,任何包含多个信息片段的对象都可以拥有自己的套接字,并且连续表单以相同的方式访问所有这些信息。

public interface IDataSocket
{
    int Count{get;}
    object this[int index]{get;}
}

我到目前为止创建的唯一套接字是 DataRow 套接字。在与数据集或其一部分交互时应使用它。例如,当您想创建“子窗体”效果时,您希望使用连续表单在父表中移动时显示所有子记录。

public class DataRowCollectionSocket: IDataSocket
{
    public System.Data.DataRowCollection Data;

    public int Count
    {
        get
        {
            return Data.Count;
        }
    }

    public object this[int index]
    {
        get
        {
            return Data[index];
        }
    }
}

可能的改进

许多,我意思是 **许多**!这旨在展示如何做到这一点。但是,它绝不是一个完整的解决方案。

  • 我们需要更多的数据套接字或一个全新的数据源无关系统。
  • 弄清楚哪些其他常用属性需要并且可以被克隆。
  • 创建数千条记录的面板可能非常耗时。应该可以先创建几个,然后在滚动到它们时创建更多。
  • 将所有内容打包成一个干净的控件,以便它能与设计器正确交互,并且您可以将控件直接拖放到滚动控件中。
  • 添加 ItemCreated 事件。
  • 为试图使用该控件但对其工作原理不感兴趣的人编写用户手册。

我相信你们能想到更多。我很想看看它的发展方向。

适用于 .NET 的连续表单 - CodeProject - 代码之家
© . All rights reserved.