Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Edit Almost Anything - Part 1

0.00/5 (No votes)
24 Sep 2004 1  
Article to accompany source for controls to edit a wide range of data types and/or complex DataSets

Correction

A small bug in SekosPD.Sekos.Windows.Forms has been corrected to fix the sizing of images that are being edited inside a datagrid. If you are using this code, you may want to download it again. Editing a range of types inside a DataGrid is covered in more detail in my new article, Get your DataGrid to Edit Almost Anything.

Introduction

There are a range of controls available as standard for Windows Forms in Visual Studio .NET 2003. These include TextBox to edit strings, CheckBox to edit booleans and even a DateTimePicker. However, there is a vast range of types available in .NET. Furthermore, new types are added all the time both by Microsoft and the development community. Only a fraction of these types will contain data that we would expect to access via a user interface. This leaves us with the significant problem of ensuring our controls are capable of handling such types.

Background

AgileStudio Object Editor

This is a screen grab of the object editor from AgileStudio, which is capable of editing a large range of data types in DataSets that may include a complex set of related tables. As a service to the community, Sekos are making freely available the source code for various controls used to accomplish this, all of which work with data binding. These include:

  • DataToolBar and DataButton which support complex binding (to a table or list) for navigation purposes
  • PropertyEditor for editing a large range of discrete types
  • DataPicture to allow the editing of Images
  • A range of additional columns for use with the DataGrid
  • A GenericEditor which can automatically generate a UI appropriate to the tables in a DataSet

We'll kick off the process in this article by examining the PropertyEditor and DataPicture controls used both standalone and within columns in a DataGrid.

PropertyEditor

How do we allow for the editing of a large range of discrete types some of which may not even have been written yet? This seems like a tall order but Microsoft has already put all the infrastructure in place for us with System.ComponentModel.TypeConverter and System.Drawing.Design.UITypeEditor. It's well documented how to go about creating a custom TypeConverter and/or UITypeEditor specific to a new data type using the TypeConverter and Editor attributes.

Among other things, this infrastructure is used by the PropertyGrid control to edit all the properties of a control at design time within Visual Studio. At runtime, it is unusual to want access to all the properties of a control. However, sometimes, it might be desirable to allow the user to adjust individual properties, for example to control the orientation of a 3D graph. (One way to handle this would be to have some kind of proxy object supporting ICustomTypeDescriptor to cut down the properties available to a PropertyGrid.) However, for maximum UI control, the best solution is to code a new control that implements IWindowsFormsEditorService. There are many aspects to supporting this service including:

  • The painting of value glyphs, for example, a colour box sample for a color editor
  • Providing a dropdown to pick from a set of standard values for enumerated types
  • Allowing custom dropdown controls or popup dialogs as appropriate.

In order for the PropertyEditor to have the appropriate behaviour when editing a specific Value, it has a PropertyType property which specifies the type of the Value. When configuring a PropertyEditor at design time, the user enters the name of a type which is converted to the actual type using 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);
}  

As you can see, this uses ITypeResolutionService in the usual case where it is called at design time. Of course, this would not be available at runtime, in which case it resorts to Type.GetType(). One thing to watch out for is that unless you give this function a full assembly qualified type name, this method only searches in the calling assembly and in mscorlib.

The final property that I want to mention is UseStringAsUnderlyingType. This is not needed when binding to properties in other controls. However, it is needed when binding to a column in a DataSet where the data type is not one of the few that are currently supported by ADO.NET. In the case of an unsupported data type, the Value is stored as a string in the data type and is converted to and from this using TypeConverter.ConvertToInvariantString() and TypeConverter.ConvertFromInvariantString(). When displaying data in the control, TypeConverter.ConvertToString() is used instead to ensure that the data format is culture specific.

So now that we've got a control that will edit any data type, we must be finished? Alas, it is never that simple. The default editor for an Image assumes we're going to embed the image in a resource file as opposed to say uploading it to a server. So we'll handle this as a special case with our next control.

DataPicture

There is a built in PictureBox control with an Image property. As it stands, it is strictly for setting images at application design time. DataPicture allows the setting of images at runtime. Unlike PictureBox, it is able to take the input focus and has a context menu to allow the transfer of images to and from the clipboard and from disk.

However, the biggest issue that needs to be overcome is the non-support of the Image data type in a DataSet. The most natural available data type is byte[] which appears as a base64binary in the DataSet schema. DataBinding, as standard, does not support conversion between byte[] and Image, but it can be enhanced using event handlers. . This is done as a two stage process. First, an event handler is implemented to receive notifications when the DataBindings collection is changed. In this handler, individual handlers are attached to the Format and Parse events for the specific DataBinding for Image if it exists. This allows us to encapsulate all this messy conversion stuff inside the control instead of requiring event handlers in the application itself.

/// 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;
}

This mechanism is best used to handle reasonably small images. However handling an image as just another part of the data does have the advantage of simplicity. There is no need to complicate the UI with upload image forms. The image will get saved with all the other data when the user presses the Save Button. This facilitates a transactional approach to data management.

Handling base64binary on the server, for example using MS SQLServer, is fairly easy but beyond the scope of the current article. I'll just mention the handy "FOR BINARY BASE64" clause of the SELECT statement which is often used in conjunction with XML.

Enhancing DataGrid

Of course, it's all very well supporting simple binding. However, most real-world applications involve lists of data, making complex bound controls such as DataGrid essential. To implement a new type of column in a DataGrid, it is neccessary to derive a new class from DataGridColumnStyle. Consequently, DataGridPropertyEditorColumn is provided to support PropertyEditor and DataGridDataPictureColumn is provided to support DataPicture. How to do this is well covered in the MS documentation so I won't go into it in detail here.

But briefly, to conserve resources, DataGrid does not create controls to populate every cell. Typically, each column will have one control that is used to interact with the user, when a cell in this column has the focus. Otherwise, a Paint() method is used to directly render the control onto a Graphics canvas. In cases like this, a call is made to a DrawData() method implemented in the appropriate control (PropertyEditor or DataPicture) in order to keep all rendering in one place. If all controls supported some interface with a method similar to this, a generic column would be possible in DataGrid which would save a great deal of effort.

Points of Interest

Sample Forms

The demo project at the start of this article includes a small MDI sample to demonstrate the editing of non-standard types with the following examples:

  • Editing properties of a control
  • Editing items in a DataSet
  • Editing within a DataGrid

Reference style documentation for the library source is available here.

The code released with this article is based on a portion of the AgileStudio product, which extends Visual Studio. Check out the free evaluation at www.sekos.com/ which automatically maintains the datasets and SQL StoreProcs required for a specific user interface (for Windows or Web applications).

Conclusion

In the next part of this series, I will discuss additional functionality for DataGrid. I will also examine data navigation and the automatic production of an intuitive user interface for a dataset with an arbitrarily complex set of tables and relations.

Please register your interest in the next part of this series by rating this article.

History

  • 24th September, 2004: Initial version

License

This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below. A list of licenses authors might use can be found here.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here