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

Continuous Forms for .NET

0.00/5 (No votes)
20 Mar 2005 5  
This coveted MS Access feature now available for .NET. The equivalent of ASP.NET's DataList for WinForms, providing a DataBound Templated control for the rich client environment. Full source code included.

Introduction

Anyone who has spent anytime developing applications in MS Access knows how RAD a RAD approach and system can be. Not only in a prototype or a simple two hour project, but even in full blown multi developer, multi tiered, enterprise edifices. The platform with all of its pre built functionality, readily accessible components and "at your fingertips" design interface, can save you thousands of man-hours not having to worry about UI issues.

In VS, Microsoft is obviously trying to offer data access to even those who can not code. If so, why in the world, if Bill Gates is paying the salary for the Access design interface designers, isn't he using them on the VS environment, is beyond my comprehension. Why, do I have to sift through a property manager to change the color of the text on my buttons or right align the text in a label, when Microsoft has the source code and design specs for a RAD like access?

Well enough on what I'd like to see from Microsoft and now what I made for them.

The one biggest loss for me when I tried moving from Access to C# was the continuous forms! We all know them. And have all used them. When you want to display many records at once but you want to layout each record, they are the perfect tools. In ASPX, it is the ever persistent "template" in Win forms, you can't find anything like it. So here it is.

Once it's been created in .NET, it is now better than it ever was in Access because now it's OOP and every records panel is a true instance. Not to mention that it's usable from every .NET language like VB or managed extensions to C++.

Understanding the control

Using reflection, I clone a Panel and all of the controls on it once for every record in a data source. So, you can scroll through many records in a data source at once. It's like presenting the data in a DataGrid but now you can layout how each record is displayed. I also fire an ItemDataBound event so that you can do custom data binding for subcontrols and contextual formatting.

All this is done in two files, with one subclassed control, two utility classes, a delegate and an interface. Totaling fewer than 200 lines of code.

The Central Class - The Control

The first built-in feature I exploit is the Panel's (finally) fine tuned ability to stretch its virtual surface and let the user scroll to any position while the programmer only needs to worry about the logical location of the controls. This is exactly how you would want a control that could display multiple records to act. So that is the control that I subclass.

public class NewPanel : System.Windows.Forms.Panel

The Properties

To make this code snippet as reusable as possible and to make it as easy as using VS's graphical layout tool, we assume that each record will be represented as a Panel and the controls directly on it. So I added a public variable (I know it should be a property) of type Panel that must be set to the Panel that will be used as a template for our records.

The variable created in the control like this:

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

will be assigned a reference to the design panel in the using project like this:

newPanel2.BasePanel = panel1;

Panel1:

The point of this control is to present (and edit) data, thus said, we must have a DataSource so a public variable was added of type IDataSocket. For a complete explanation of my data agnosticism solution, see the end of this article. For the mean time, just use one of the supplied sockets like the RowCollectionSocket which is built to interface with parts of a DataSet.

The variable created in the control like this:

public IDataSocket DataSource = null;

will be assigned a reference to the Data Container in the using project like this:

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

The controls on the Panel being used as the template should have DataBindings to the fields in the same data container used as the DataSource for our Panel. All simple DataBindings for any property should work.

In code, that may look like this:

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"));

or graphically like this:

Bind Function

The rest of the work is done in the Bind() function with the help of a private function DeepCloneControl().

For some reason, you cant foreach through the Controls collection, so I copy references to the controls into an array. I then keep that same array around for all the rows of data.

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

The Controls collection is also missing a way to get a child by name. This can pose a problem when you are trying to access things in the ItemDataBound event. So I create a Hashtable to store the ordinal of every control in the array that I can use later to access any control I want through the Controls collection's indexer. This nifty little snippet is wrapped in the ControlFromName() function of the EventArgs class.

Map creation:

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

Function to use map:

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

We then get what we are all here for, the looping through the data. For each row (item), I clone the Panel that is being used for the template. I add the new Panel to the Scrolling Panel. The only property I change is the position, so that it lines up right under the previous Panel (you may want to add a property to set the spacing). I loop through the controls and clone each one. Finally, I raise the ItemDataBound event and get on to the next row (item) in the 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 Function

All the reflection needs to clone non-cloneable objects is accessible through the GetType function (and the Type associated with the class/object/control). Once I have a type object, almost all the work is done with the subclasses of MemberInfo.

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

At that point, all I need to do is create (through reflection / the ConstructorInfo) the same type of control and store it in a variable of type Control (because I know it must inherit from that). The next step is to loop through every safe property and copy the value from the source (master / template) control to the same property in the newly created control. The way I determine safe is by taking only those properties that are of a value-type or of type string. Trying to copy references to (and for sure trying to clone) anything else can prove to be catastrophic. Imagine all controls writing to the same graphics object. More often than not you will just crash your program. I could keep a list of safe properties but I hate special case code, I feel it reveals a flaw in the design and I use them only when I have no control and no options. That being said, I have a second loop for creating DataBindings for the new control, and I check for a property called DataSource and copy the reference to the data container specified therein, so that controls like combo boxes will still have their RowSource info.

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

The DataBindings are created by getting the field name from the source (master / template) control, by parsing the string, and assigning the current row (item) as the DataSource.

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

ContinuousItemDataBound & ContinuousItemEventArgs

These two items come together to bring you the control of per row (record / item) changes. The event is fired once for every row (record / item) at the end of all the processing. It gives you a chance - with the help of the EventArgs - to analyze that particular data item and change the Panel presenting that item however you want to.

Members

The EventArgs has three public properties and one function. The properties contain references to the Panel created for this data item, the particular data item, and the name to collection ordinal map that is used by the one function to give you access to any control on the Panel.

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

IDataSocket & DataRowCollectionSocket

The IDataSocket is my way of being able to deal with any type of data source that can hold many items of data. Why is it that not all these classes inherit from a common interface is beyond me. It would seem only logical that every collection, table, stream that can be looped through should inherit from the same interface and that interface should have the members needed to do just that. Then there should be an interface for any class that exposes an indexer. I also didn't want my code cluttered with casts, so I killed two birds with one stone, and created an interface that has a count property and an indexer. Now any object that has multiple pieces of information can have its own socket and the continuous forms access them all in the same way.

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

The only socket I have created so far is the DataRow one. It should be used whenever interacting with a dataset or a portion thereof. Like the child rows of the current record when you want to create the "subform" effect, where you want to use the continuous forms to display all of the child records when moving through the parent table.

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

Possible Improvements

Many and I mean many! This is intended to demonstrate how this could be done. However, it is in no way a complete solution.

  • We need more DataSockets or a completely new data source agnosticism system.
  • Figure out what other commonly used properties need to and can be cloned.
  • Creating panels for thousands of records can be time consuming. It should be easy to create a few in the beginning and then create more as you scroll to them.
  • Package it all into a clean control so that it interacts properly with the designer and you can just drop the controls into the scrolling control.
  • Adding the ItemCreated event.
  • Writing a user manual for someone trying to use the control but not interested in how it works.

I am sure you guys can think of much more. I'd love to see where this goes.

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