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

编辑几乎所有内容 - 第1部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (17投票s)

2004 年 9 月 24 日

8分钟阅读

viewsIcon

107939

downloadIcon

3773

本文档配套源代码,用于编辑各种数据类型和/或复杂 DataSet 的控件。

更正

已更正 SekosPD.Sekos.Windows.Forms 中的一个小 bug,该 bug 会影响在 datagrid 中编辑的图像的尺寸。如果您正在使用此代码,则可能需要重新下载。

在我新的文章 “让您的 DataGrid 编辑几乎任何内容” 中,更详细地介绍了在 DataGrid 中编辑各种类型的内容。

引言

Visual Studio .NET 2003 中的 Windows Forms 提供了一系列标准控件。其中包括用于编辑 stringTextBox,用于编辑布尔值的 CheckBox,甚至还有 DateTimePicker。然而,.NET 中存在着种类繁多的类型。此外,微软和开发社区不断地添加新类型。只有一小部分这些类型的数据是我们期望通过用户界面访问的。这就给我们留下了确保我们的控件能够处理这些类型的重大问题。

背景

AgileStudio Object Editor

这是 AgileStudio 对象编辑器的屏幕截图,该编辑器能够编辑 DataSet 中种类繁多的数据类型,这些数据类型可能包含一组复杂的关联表。作为对社区的服务,Sekos 免费提供用于实现此目的的各种控件的源代码,所有这些控件都支持数据绑定。其中包括:

  • DataToolBarDataButton,它们支持用于导航的复杂绑定(到表或列表)
  • PropertyEditor,用于编辑大量离散类型
  • DataPicture,用于编辑图像
  • 一系列用于 DataGrid 的附加列
  • GenericEditor,可以自动为 DataSet 中的表生成合适的 UI

在本文中,我们将首先介绍单独使用以及在 DataGrid 列中使用 PropertyEditorDataPicture 控件。

PropertyEditor

我们如何才能编辑大量离散类型,其中一些类型甚至可能还没有被编写出来?这似乎是一个艰巨的任务,但微软已经为我们准备好了所有基础设施:System.ComponentModel.TypeConverterSystem.Drawing.Design.UITypeEditor。使用 TypeConverterEditor 属性来创建针对新数据类型的自定义 TypeConverter 和/或 UITypeEditor 的方法已有详细的文档说明。

除其他外,此基础设施由 PropertyGrid 控件在 Visual Studio 中的设计时使用,用于编辑控件的所有属性。在运行时,我们通常不希望访问控件的所有属性。但是,有时允许用户调整单个属性可能是可取的,例如控制 3D 图表的方向。(处理此问题的一种方法是使用某种支持 ICustomTypeDescriptor 的代理对象,以减少 PropertyGrid 可用的属性。)但是,为了实现最大的 UI 控制,最佳解决方案是编写一个实现 IWindowsFormsEditorService 的新控件。支持此服务有很多方面,包括:

  • 绘制值图标,例如,颜色编辑器的颜色框样本
  • 为枚举类型提供一个下拉列表以从中选择一组标准值
  • 根据需要提供自定义下拉控件或弹出对话框。

为了使 PropertyEditor 在编辑特定 Value 时具有适当的行为,它有一个 PropertyType 属性,用于指定 Value 的类型。在设计时配置 PropertyEditor 时,用户输入一个类型名称,该名称通过 TypeTypeConverter.ConvertFrom() 转换为实际类型。

public override object ConvertFrom(ITypeDescriptorContext context,
 System.Globalization.CultureInfo culture, object value)
{
    if (value is string)
    {
        System.ComponentModel.Design.ITypeResolutionService typeResolver=
         (System.ComponentModel.Design.ITypeResolutionService)context.GetService(
         typeof(System.ComponentModel.Design.ITypeResolutionService));
        Type t= null;
        string typeName= (string)value;
        if (typeResolver!=null)
            t= typeResolver.GetType(typeName);
        else
        {
            // In the unlikely case that this property is bound to another
            // PropertyEditor
            // so it can be changed at runtime, ITypeResolutionService won't
            // be available
            // so we resort to GetType instead.
            t= Type.GetType(typeName);
            // Hard Code System.Drawing in as an additional assembly to search
            if (t==null)
                t= typeof(System.Drawing.Color).Assembly.GetType(typeName);
        }
        return t;
    }
    return base.ConvertFrom (context, culture, value);
}  

如您所见,这在通常情况下(在设计时调用)使用了 ITypeResolutionService。当然,这在运行时将不可用,在这种情况下,它会回退到 Type.GetType()。需要注意的一点是,除非为此函数提供完整的程序集限定类型名称,否则此方法仅在调用程序集和 mscorlib 中进行搜索。

我想提到的最后一个属性是 UseStringAsUnderlyingType。在绑定到其他控件的属性时不需要此属性。但是,当绑定到 DataSet 中的列且数据类型不是 ADO.NET 当前支持的少数类型之一时,就需要此属性。对于不支持的数据类型,Value 将在数据类型中存储为 string,并通过 TypeConverter.ConvertToInvariantString()TypeConverter.ConvertFromInvariantString() 进行转换。在控件中显示数据时,将使用 TypeConverter.ConvertToString() 以确保数据格式特定于区域性。

因此,现在我们有了一个可以编辑任何数据类型的控件,我们是不是就完成了?唉,事情从来没有这么简单。Image 的默认编辑器假定我们将图像嵌入资源文件中,而不是将其上传到服务器。因此,我们将把这种情况作为特殊情况来处理,并介绍下一个控件。

DataPicture

有一个内置的 PictureBox 控件,其中包含一个 Image 属性。就其本身而言,它严格用于在应用程序设计时设置图像。DataPicture 允许在运行时设置图像。与 PictureBox 不同,它可以获取输入焦点,并具有一个上下文菜单,允许将图像复制到剪贴板和从剪贴板复制,以及从磁盘复制和从磁盘复制。

然而,需要克服的最大问题是 DataSet 中对 Image 数据类型的支持不足。最自然的可用数据类型是 byte[],它在 DataSet 架构中显示为 base64binaryDataBinding 标准不支持 byte[]Image 之间的转换,但可以使用事件处理程序对其进行增强。这是一个分两步完成的过程。首先,实现一个事件处理程序,用于在 DataBindings 集合发生更改时接收通知。在此处理程序中,为特定 ImageDataBindingFormatParse 事件附加单独的事件处理程序(如果存在)。这允许我们将所有这些混乱的转换代码封装在控件内部,而不是要求在应用程序本身中编写事件处理程序。

/// Construct a DataPicture
public DataPicture()
{
    ...
    // Register an interest in the DataBindings collection
    this.DataBindings.CollectionChanged+=new CollectionChangeEventHandler(
     DataBindings_CollectionChanged);
}

// Check whether a Binding for Image has been added and if so
// attach event handlers to handle data conversion
private void DataBindings_CollectionChanged(object sender,
 CollectionChangeEventArgs e)
{
    Binding binding = this.DataBindings["Image"];
    if (binding!=null)
    {    binding.Format+=new ConvertEventHandler(binding_Format); 
        binding.Parse+=new ConvertEventHandler(binding_Parse);
    }
}

// An event handler to handle the conversion from binary (byte[]) to Image
// of data entering the control
private void binding_Format(object sender, ConvertEventArgs ev)
{
    if (!(ev.Value is Image) && ev.DesiredType==typeof(Image))
    {
        byte[] img = ev.Value as byte[];
        ev.Value= sEmptyBitmap;
        if (img!=null && img.Length>0)
        {
            System.IO.MemoryStream ms = new System.IO.MemoryStream();
            try
            {
                int offset = 0;
                ms.Write(img, offset, img.Length - offset);
                Bitmap bmp = new Bitmap(ms);
                ms.Close();
                ev.Value= bmp;
            }
            catch (Exception)
            {
            }
        }
    }
}

// An event handler to handle the conversion from Image to binary (byte[])
// of data leaving the control
private void binding_Parse(object sender, ConvertEventArgs ev)
{
    if ((ev.Value is Bitmap) && ev.DesiredType==
    typeof(byte[]))
        { Bitmap bmp=
        (Bitmap)ev.Value;
            System.IO.MemoryStream ms = new System.IO.MemoryStream();
            System.Drawing.Imaging.ImageFormat format= bmp.RawFormat;
            if (format==
                System.Drawing.Imaging.ImageFormat.MemoryBmp) format=
            System.Drawing.Imaging.ImageFormat.Bmp;
            bmp.Save(ms,format);
            ms.Close(); ev.Value=
        (Byte[])ms.GetBuffer();
    }
    else if (ev.Value==null || ev.Value==sEmptyBitmap)
        ev.Value=System.DBNull.Value;
}

此机制最适合处理相对较小的图像。然而,将图像作为数据的另一个部分来处理确实具有简单性的优点。无需用图像上传表单使 UI 复杂化。当用户按 **保存** 按钮时,图像将与其他所有数据一起保存。这促进了数据管理的事务性方法。

在服务器端处理 base64binary(例如,使用 MS SQLServer)相对容易,但超出了当前文章的范围。我只提一下 SELECT 语句中常用的方便的 "FOR BINARY BASE64" 子句,它通常与 XML 一起使用。

增强 DataGrid

当然,支持简单绑定固然很好。然而,大多数实际应用程序都涉及数据列表,这使得 DataGrid 等复杂绑定控件变得至关重要。要为 DataGrid 实现一种新的列类型,就需要从 DataGridColumnStyle 派生一个新类。因此,提供了 DataGridPropertyEditorColumn 来支持 PropertyEditor,并提供了 DataGridDataPictureColumn 来支持 DataPicture。如何实现这一点在 MS 文档中有详细介绍,因此我在此不再赘述。

但简而言之,为了节约资源,DataGrid 不会为每个单元格创建控件。通常,每列有一个控件用于与用户交互,当该列中的某个单元格获得焦点时。否则,将使用 Paint() 方法直接将控件渲染到 Graphics 画布上。在这种情况下,将调用适当控件(PropertyEditorDataPicture)中实现的 DrawData() 方法,以将所有渲染保持在一个位置。如果所有控件都支持某种具有类似方法的接口,那么在 DataGrid 中就可以实现一个通用的列,这将节省大量工作。

关注点

Sample Forms

本文开头的演示项目包含一个小型 MDI 示例,用于演示使用以下示例编辑非标准类型:

  • 编辑控件的属性
  • 编辑 DataSet 中的项
  • DataGrid 中编辑

文档的参考风格的库源代码可在 此处获得。

本文随附的代码基于 AgileStudio 产品的一部分,该产品扩展了 Visual Studio。请查看在 www.sekos.com/ 上的免费试用版,它会自动维护特定用户界面(用于 Windows 或 Web 应用程序)所需的数据集和 SQL StoreProcs。

结论

在本系列的下一部分中,我将讨论 DataGrid 的附加功能。我还将探讨数据导航以及为具有任意复杂表和关系的 dataset 自动生成直观的用户界面。

请通过评价本文来表达您对本系列下一部分的兴趣。

历史

  • 2004 年 9 月 24 日:初始版本

许可证

本文没有明确的许可证,但可能包含文章文本或下载文件本身的使用条款。如有疑问,请通过下方的讨论区联系作者。您可以在 此处找到作者可能使用的许可证列表。

© . All rights reserved.