Example of Removing Some Pain: Grid Fluent API

So on a previous project, we were making use a 3rd party WinForms UberGrid which, of course has a monstrous designer that spits out tons of code into your InitializeComponent() method in your .designer file. I think you know what I’m talking about, but for those who wish to experience it in all its glory, check out this:

Infragistics.Win.Appearance appearance4 = new Infragistics.Win.Appearance();
Infragistics.Win.Appearance appearance1 = new Infragistics.Win.Appearance();
Infragistics.Win.Appearance appearance2 = new Infragistics.Win.Appearance();
Infragistics.Win.Appearance appearance3 = new Infragistics.Win.Appearance();
Infragistics.Win.Appearance appearance7 = new Infragistics.Win.Appearance();
Infragistics.Win.Appearance appearance6 = new Infragistics.Win.Appearance();
Infragistics.Win.Appearance appearance5 = new Infragistics.Win.Appearance();
Infragistics.Win.Appearance appearance9 = new Infragistics.Win.Appearance();
Infragistics.Win.Appearance appearance11 = new Infragistics.Win.Appearance();
Infragistics.Win.Appearance appearance10 = new Infragistics.Win.Appearance();
Infragistics.Win.Appearance appearance8 = new Infragistics.Win.Appearance();
this._lineItemsGrid = new Infragistics.Win.UltraWinGrid.UltraGrid();
((System.ComponentModel.ISupportInitialize)(this._lineItemsGrid)).BeginInit();
this.SuspendLayout();
// 
// _lineItemsGrid
// 
appearance4.BackColor = System.Drawing.SystemColors.Window;
appearance4.BorderColor = System.Drawing.SystemColors.InactiveCaption;
this._lineItemsGrid.DisplayLayout.Appearance = appearance4;
this._lineItemsGrid.DisplayLayout.AutoFitStyle = Infragistics.Win.UltraWinGrid.AutoFitStyle.None;
this._lineItemsGrid.DisplayLayout.BorderStyle = Infragistics.Win.UIElementBorderStyle.Solid;
this._lineItemsGrid.DisplayLayout.CaptionVisible = Infragistics.Win.DefaultableBoolean.False;
this._lineItemsGrid.DisplayLayout.EmptyRowSettings.ShowEmptyRows = true;
appearance1.BackColor = System.Drawing.SystemColors.ActiveBorder;
appearance1.BackColor2 = System.Drawing.SystemColors.ControlDark;
appearance1.BackGradientStyle = Infragistics.Win.GradientStyle.Vertical;
appearance1.BorderColor = System.Drawing.SystemColors.Window;
this._lineItemsGrid.DisplayLayout.GroupByBox.Appearance = appearance1;
appearance2.ForeColor = System.Drawing.SystemColors.GrayText;
this._lineItemsGrid.DisplayLayout.GroupByBox.BandLabelAppearance = appearance2;
this._lineItemsGrid.DisplayLayout.GroupByBox.BorderStyle = Infragistics.Win.UIElementBorderStyle.Solid;
this._lineItemsGrid.DisplayLayout.GroupByBox.Hidden = true;
appearance3.BackColor = System.Drawing.SystemColors.ControlLightLight;
appearance3.BackColor2 = System.Drawing.SystemColors.Control;
appearance3.BackGradientStyle = Infragistics.Win.GradientStyle.Horizontal;
appearance3.ForeColor = System.Drawing.SystemColors.GrayText;
this._lineItemsGrid.DisplayLayout.GroupByBox.PromptAppearance = appearance3;
this._lineItemsGrid.DisplayLayout.MaxColScrollRegions = 1;
this._lineItemsGrid.DisplayLayout.MaxRowScrollRegions = 1;
appearance7.BackColor = System.Drawing.SystemColors.Highlight;
appearance7.ForeColor = System.Drawing.SystemColors.HighlightText;
this._lineItemsGrid.DisplayLayout.Override.ActiveRowAppearance = appearance7;
this._lineItemsGrid.DisplayLayout.Override.AllowAddNew = Infragistics.Win.UltraWinGrid.AllowAddNew.No;
this._lineItemsGrid.DisplayLayout.Override.AllowColMoving = Infragistics.Win.UltraWinGrid.AllowColMoving.NotAllowed;
this._lineItemsGrid.DisplayLayout.Override.AllowColSizing = Infragistics.Win.UltraWinGrid.AllowColSizing.Free;
this._lineItemsGrid.DisplayLayout.Override.AllowColSwapping = Infragistics.Win.UltraWinGrid.AllowColSwapping.NotAllowed;
this._lineItemsGrid.DisplayLayout.Override.AllowDelete = Infragistics.Win.DefaultableBoolean.False;
this._lineItemsGrid.DisplayLayout.Override.AllowRowFiltering = Infragistics.Win.DefaultableBoolean.False;
this._lineItemsGrid.DisplayLayout.Override.AllowRowSummaries = Infragistics.Win.UltraWinGrid.AllowRowSummaries.False;
this._lineItemsGrid.DisplayLayout.Override.AllowUpdate = Infragistics.Win.DefaultableBoolean.False;
this._lineItemsGrid.DisplayLayout.Override.BorderStyleCell = Infragistics.Win.UIElementBorderStyle.Dotted;
this._lineItemsGrid.DisplayLayout.Override.BorderStyleRow = Infragistics.Win.UIElementBorderStyle.Dotted;
appearance6.BackColor = System.Drawing.SystemColors.Window;
this._lineItemsGrid.DisplayLayout.Override.CardAreaAppearance = appearance6;
appearance5.BorderColor = System.Drawing.Color.Silver;
appearance5.FontData.Name = "QuickType Mono";
appearance5.FontData.SizeInPoints = 10F;
appearance5.TextTrimming = Infragistics.Win.TextTrimming.EllipsisCharacter;
this._lineItemsGrid.DisplayLayout.Override.CellAppearance = appearance5;
this._lineItemsGrid.DisplayLayout.Override.CellClickAction = Infragistics.Win.UltraWinGrid.CellClickAction.RowSelect;
this._lineItemsGrid.DisplayLayout.Override.CellPadding = 0;
this._lineItemsGrid.DisplayLayout.Override.ColumnSizingArea = Infragistics.Win.UltraWinGrid.ColumnSizingArea.EntireColumn;
appearance9.BackColor = System.Drawing.SystemColors.Control;
appearance9.BackColor2 = System.Drawing.SystemColors.ControlDark;
appearance9.BackGradientAlignment = Infragistics.Win.GradientAlignment.Element;
appearance9.BackGradientStyle = Infragistics.Win.GradientStyle.Horizontal;
appearance9.BorderColor = System.Drawing.SystemColors.Window;
this._lineItemsGrid.DisplayLayout.Override.GroupByRowAppearance = appearance9;
appearance11.TextHAlignAsString = "Left";
this._lineItemsGrid.DisplayLayout.Override.HeaderAppearance = appearance11;
this._lineItemsGrid.DisplayLayout.Override.HeaderStyle = Infragistics.Win.HeaderStyle.WindowsXPCommand;
appearance10.BackColor = System.Drawing.SystemColors.Window;
appearance10.BorderColor = System.Drawing.Color.Silver;
this._lineItemsGrid.DisplayLayout.Override.RowAppearance = appearance10;
this._lineItemsGrid.DisplayLayout.Override.RowSelectorHeaderStyle = Infragistics.Win.UltraWinGrid.RowSelectorHeaderStyle.SeparateElement;
this._lineItemsGrid.DisplayLayout.Override.RowSelectors = Infragistics.Win.DefaultableBoolean.True;
this._lineItemsGrid.DisplayLayout.Override.RowSizing = Infragistics.Win.UltraWinGrid.RowSizing.Fixed;
this._lineItemsGrid.DisplayLayout.Override.SelectTypeCell = Infragistics.Win.UltraWinGrid.SelectType.None;
this._lineItemsGrid.DisplayLayout.Override.SelectTypeCol = Infragistics.Win.UltraWinGrid.SelectType.None;
this._lineItemsGrid.DisplayLayout.Override.SelectTypeRow = Infragistics.Win.UltraWinGrid.SelectType.Single;
this._lineItemsGrid.DisplayLayout.Override.SummaryDisplayArea = Infragistics.Win.UltraWinGrid.SummaryDisplayAreas.None;
appearance8.BackColor = System.Drawing.SystemColors.ControlLight;
this._lineItemsGrid.DisplayLayout.Override.TemplateAddRowAppearance = appearance8;
this._lineItemsGrid.DisplayLayout.ScrollBounds = Infragistics.Win.UltraWinGrid.ScrollBounds.ScrollToFill;
this._lineItemsGrid.DisplayLayout.ScrollStyle = Infragistics.Win.UltraWinGrid.ScrollStyle.Immediate;
this._lineItemsGrid.Dock = System.Windows.Forms.DockStyle.Top;
this._lineItemsGrid.Location = new System.Drawing.Point(0, 0);
this._lineItemsGrid.Name = "_lineItemsGrid";
this._lineItemsGrid.Size = new System.Drawing.Size(908, 226);
this._lineItemsGrid.TabIndex = 0;
this._lineItemsGrid.Text = "_lineItemsGrid";

Rather than having to deal with all that code and all the potential for inconsistencies in style, behavior, etc in all the various screens in which grids would appear, we decided to wrap the bulk of the building of appearances into a fluent API that ended up looking something like this:

new GridLayout(_lineItemsGrid)
    .AutoFitColumns()
    .EditingDisabled()
    .AddColumn("Id").ThatIsHidden()
    .AddColumn("Description").WithWidth(225)
    .AddColumn("Quantity").WithWidth(40).FormatValueAs("F4").AlignRight()
    .AddColumn("Size").WithWidth(50).AlignRight().FormatValueAs("F4")
    .AddColumn("Price").WithWidth(50).AlignRight().FormatValueAs("C4")
    .OnRowSelected((values) => _model.ViewItem(values["Id"]))
    .OnRowDoubleClick((values) => _model.EditItem(values["Id"]));

Now, when a feature came in for us to change certain styles or behavior of all the grids (i.e. make the headers look like THIS) we changed it in one place and it affected everything.

We considered using a grid-wrapping UserControl for this type of reuse and behavior, but it turns out that about half of the code was common among all grids and half was specific to each form. So we would’ve had to build a kitchen-sink type UserControl that ultimately would’ve taken us down the path of exposing much of the raw grid API to each form which was not as desirable.

Other benefits we gained by this approach:

  1. Testing was much easier because the GridLayout builder could embed certain controls and special tags on controls in order to highlight things for StoryTeller/Fit testing.  Also, we could test that certain conventions were being followed by all the forms rather than having each form build its own grid and having to test that it did it correctly, we could just test/enforce that the GridLayout builder worked properly.
  2. When we started making the switch to WPF, we were able to switch to a WPF grid control with only a one-line change to each view (to change the type declaration of _lineItemsGrid).
  3. Easier integration with various services like user permissions (i.e. certain users can never see the ‘Margin’ column in any grid)

Building a Fluent Interface To Drive Settings On Another Object

Building a fluent interface like this was pretty easy: just make each call return the instance of GridLayout! 

Here’s an example method:

public GridLayout AutoFitColumns()
{
    _grid.DisplayLayout.AutoFitStyle = AutoFitStyle.ResizeAllColumns;
    return this;
}

Building up the columns required that I remember the last column that was added so I can keep context.

For example:

public GridLayout AddColumn(string key)
{
    _lastColumn = _grid.DisplayLayout.Bands[0].Columns.Add(key);
    return this;
}

public GridLayout ThatIsHidden()
{
    _lastColumn.Hidden = true;
    return this;
}

public GridLayout Header(string header)
{
    _lastColumn.Header.Caption = header;
    return this;
}

Related Articles:

    Post Footer automatically generated by Add Post Footer Plugin for wordpress.

    About Chad Myers

    Chad Myers is the Director of Development for Dovetail Software, in Austin, TX, where he leads a premiere software team building complex enterprise software products. Chad is a .NET software developer specializing in enterprise software designs and architectures. He has over 12 years of software development experience and a proven track record of Agile, test-driven project leadership using both Microsoft and open source tools. He is a community leader who speaks at the Austin .NET User's Group, the ADNUG Code Camp, and participates in various development communities and open source projects.
    This entry was posted in .NET, Fluent API. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
    • http://www.potschka-it.de Florian Potschka

      How true. Especially GUI code often tends to be extreme verbose and repetitive. So a fluent API is a great benefit for this kind of code.

    • Macca

      Chad,

      Nice, thanks for taking the time to elaborate on your previous post (and so quickly too!). Very useful indeed.

    • Jeff Certain

      Chad,

      I just finished doing something similar for a bunch of our “helper methods” for these same controls. I’d been wondering about the Appearance verbosity, and now I’ve got a good pointer for the right road to go down.

      Thanks!

      Jeff

    • http://www.derickbailey.com Derick Bailey

      Is it worth creating an IGridLayout interface that can be injected, so that this layout code can be unit tested? (i have my own opinion, but want to see what you think)

    • http://chadmyers.lostechies.com Chad Myers

      Derick:

      You could do that as an interaction test. What we ended up doing was just checking that the Grid had the columns we expected in the end. I was less concerned with the interaction with the GridLayout FI than I was with the end state (in this case).

      It made for easier state based testing so we could do things like:

      layoutForm.Grid.ShouldHaveColumn(“Description”);

    • http://www.derickbailey.com Derick Bailey

      that’s basically what I was thinking, too. The only reason I would bother with DI for testability would be when the layout of the control was truly really a business concern. Off-hand, though, I can’t think of any real examples where the layout would be the business concern, except for the business of custom controls.