Datagrids are great for a rigid display of rows and columns, but what about flexibility?
Here is your average datagrid:

Here is a simple example of what's possible using a flowpanel:

Features:
- Databinding
- Detail Control
- Reflection for building detail controls
- Base detail control
Requirements:
Notes:
For starters, this is a preliminary foray into what's possible using this technique. This is not exhaustive, and may not be the most efficient either. For example, the base classes and reflection usage could be refactored further, and There are not many comments in the code.
On Features:
Databinding: Both a curse and a blessing to developers. I have one bug that is not solved yet. Both the qty and price cells are databound to an LLBLGEN entity, and the underlying datasource (the entity) do not recieve the value on the first try. So on the second try, it will work.
But databinding is great when it works. You'll notice that the totals automatically update. This calculation occurs within the LLBLGEN entity.
FlowPanel: I was trying very hard to make the TablePanel work, but the FlowPanel got it perfect on the first try.
Reflection: What fun, we get to use reflection! The benefit of this is that we can specify the control to be repeated inside of the flow panel by its type with a string value.
On requirements:
Northwind: I modified the order details table to include a comment column. This shows of the two line order detail approach that I've implemented.
LLBLGEN: I already have some experience with this framework, so I'm going to be using it for all my data calls. True I could use datatables, table adapters, and sql connections, but then again, you could be writing html & ajax using just visual studio or notepad. I think LLBLGEN gives cleaner code, along with the benefits of being strongly typed. All the controls are databound to llblgen entities.
The dlls for this are already included in the demo, for your enjoyment :)
If you have ever spent time refining sql queries, perameters, filters, table adapters, not to mention a schema that is changing, you owe it to yourself to discover the amazing wonder that is LLBLGEN!
Caching: In my quick sample, each Order Detail line has a combobox with a datasource. Caching makes sense here, as we don't want to be making multiple data calls. I'm not going to use any dependancies, to keep this simple.
SQL Server: The LLBLGEN entities and the sql update script were built against a sql 2008 database. I'm not sure what will happen when using this code against a 2000 or 2005 database, but hopefully no complications will arise.
On Installation:
- Unpack ZIP file
- Install Northwind Database on SQL Server 00/05/08
- Execute included sql script (sqlUpdateScript.sql)
- Modify key Main.ConnectionString in Web.Config to match your sql database
- if your catalog name differs from "Northwind" please see this post.
- Build
On Usage:
The initial form should be the OrderChooserForm. If you click on a row, and then click button1, an order window will pop up.
The Code:
Without going into too much detail, here are the inner working. As I noted earlier, there may be a more efficient design, this is only a starting point. Additionally, this is a read-only design so far.
There are 3 main elements to this:
- The Detail Control
- The layout panel hosting a collection of detail controls
- The main form hosting the layout panel
1. The Detail Control
I created a base class - GridDetailBaseControl that contains some events and an Index property.
The OrderDetailControl contains UI elements

and a minimum amount of code.
The detail control is instantiated like this:
27 public OrderDetailControl(OrderDetailsEntity orderDetailEntity, int index) : base(index)
28 {
29 InitializeComponent();
30 productsEntityCollection = HttpContext.Current.Cache["AllProductsEntityCollection"] as EntityCollection;
31 productComboBox.DataSource = productsEntityCollection;
32 orderDetailBindingSource.DataSource = orderDetailEntity;
33 fixComboboxBinding();
34
35 formLoaded = true;
36 }
For simplicity sake, I am taking in my LLBLGEN entity, as well as the index, and initializing all of my databinding at the beginning.
When the price text box or the qty text box changes, the refresh method is called which gets the row total calculation from the entity:
44 private void refresh()
45 {
46 orderDetailBindingSource.EndEdit();
47 orderDetailBindingSource.ResetCurrentItem();
48 }
When the row total text value changes, the rowChanged event is called, which is encapsulated in the base class:
80 private void totalTextBox_TextChanged(object sender, EventArgs e)
81 {
82 if (formLoaded)
83 {
84 OnRowChanged(sender, e);
85 }
86 }
The layout panel handles this event and throws it up to the main form which is able to then recalculate the order total.
The OrderDetailControl also contains a static member of its type (useful for reflection):
24 public static string TypeName = "DetailItemsDemo.OrderDetailControl";
2. The Layout Panel
The layout panel is named a GridFlowPanel, which isn't the best name, but anyway, it inherits from a FlowLayoutPanel.
The controls are drawn in this method:
33 public void DrawControls(IListSource data, Type controlType)
34 {
35 IList datasource = (IList)data;
36 for (int i = 0; i < ((IList)data).Count ; i++)
37 {
38 // create control
39 GridDetailBaseControl controlToAdd = (GridDetailBaseControl)Activator.CreateInstance(controlType, new object[] { datasource[i],i });
40 // wire up rowchanged event
41 controlToAdd.RowChanged += delegate(object sender, EventArgs e)
42 {
43 if (RowChanged != null)
44 RowChanged.Invoke(sender, e);
45 };
46
47 Controls.Add(controlToAdd);
48 }
49 }
This method takes in the list of the data and the type of control to create, and then loops through the list, creating a control for each line. Each data object in the list in passed, along with the index value, to the constructor of the new control. Note that the constructor perameters have to match your object array, or a runtime error will occur.
It's also worth noting that the rowchanged event for that detail control is wired up at this point on line 41. So when the row throws a RowChanged event, the flowControl will Raise its own RowChanged event to the form.
There is an override that takes in a string for the control name:
22 public void DrawControls(IListSource data, string assemblyFile, string typeName)
23 {
24 Type controlType;
25
26 if (assemblyFile != string.Empty)
27 controlType = Assembly.LoadFrom(assemblyFile).GetType(typeName);
28 else
29 controlType = Assembly.GetExecutingAssembly().GetType(typeName);
30
31 DrawControls(data, controlType);
32 }
3. Main Form
The main form does a few important things:
- fetch data
- init rows
- perform calculations on the fly
Fetch Data:
68 private void FetchData(int orderNumber)
69 {
70 using (DataAccessAdapter adapter= new DataAccessAdapter())
71 {
72 CurrentEntity = (DataAccess.GetOrderEntity(orderNumber, adapter));
73 DataAccess.GetProducts(adapter);
74 }
75 orderEntityCollection.Add(CurrentEntity);
76 Text = String.Format("Sales Order: {0}", orderNumber.ToString());
77 }
We're using LLBLGEN here to get the order entity (which also includes the customer and the order detail items), and the Products. I should also mention that the method executed on line 73 will also cache the list of products, which is used by the OrderDetail row as the datasource for the combobox.
Here is the row initialization:
78 public void FillTable()
79 {
80 gridFlowPanel1.DrawControls(CurrentEntity.OrderDetails, OrderDetailControl.TypeName);
81 statusBar1.Text = String.Format("{0} rows", gridFlowPanel1.Controls.Count);
82 }
There is not much to it, just supply the list of data, and the string name of the order detail control. We are using the executing assembly here, if you had controls in another assembly or project, you would probably have to use another overload that I haven't tested yet.
Finally, when the order total changes on a row, the RowChanged event on the flowControl is handled, and the CalculateOrder method is executed:
58 private void CalculateOrderTotal()
59 {
60 decimal orderTotal = 0;
61 for (int i = 0; i < CurrentEntity.OrderDetails.Count; i++)
62 {
63 orderTotal += CurrentEntity.OrderDetails[i].ExtPrice;
64 }
65 this.orderTotalTextBox.Text = String.Format("{0:C}",orderTotal);
66 }
This is a benefit of LLBLGEN, you have strongly typed objects, so you can loop through the collection of bound orderdetail objects, and get your new order total.
And that's it. Hopefully you enjoyed the article and found some illumination. Good luck!
Download Code
Note: Codes are submitted as a .zip file to shorten your download time. After downloading it, you will need a program like Winzip to decompress it.
Terms of Agreement:
By using this code, you agree to the following terms...
- You may use this code in your own programs (and may compile it into a program and distribute it in compiled format for languages that allow it) freely and with no charge.
- You MAY NOT redistribute this code (for example to a web site) without written permission from the original author. Failure to do so is a violation of copyright laws.
- You may link to this code from another website, but ONLY if it is not wrapped in a frame.
- You will abide by any additional copyright restrictions which the author may have placed in the code or code's description.