Saturday, November 27, 2010

Redefining bound columns in Infragistic's UltraWinGrid

This week I had to implement a tree like grid in windows forms, ideally these were my wishes:

  1. I wanted to make usage of the bindings features of this grid, in other words, maintaining my model synchronized automatically by the grid
  2. Be it tree like because I need to bind to a recursive model (multiple nested bands of different model types)
  3. Some of these bands can be edited directly in the grid, and others that are more complex are edited through other form, so some bands are editable and others are read only
  4. Change the form (including the grid) from readonly to editable mode and back, putting every cell in editable doesn't cut it for me, because as the point just above mentions, some of my fields are never editable.


I had some experience with infragistics, but only with the web grid so probably some of the things I'm doing could be done easier by one of the multiple options in the grid that I couldn't find while I was researching about this, so if anyone knows how to do something in an easier way I would be glad to know it!


So, from my research and tests, when you assign a datasource to the UltraWinGrid there is no -direct- way of telling it what columns to show or hide, it will show everything available in the datasource you provide, this is far from ideal in my case, e.g. my model classes have "Id" columns or other columns that my client has specified he doesn't want to see in the grid. So if you want to hide or change something on the grid's layout, you should do it on the InitializeLayout event.

After playing a little with the layout in that event, I realized that it was a lot of work and the code wasn't very elegant, so I've decided to create an abstraction that could help me with this problem, and also be able to reuse in other screens where I know I will need the same. I've started thinking on how I would like to use this new abstraction and ended with something like this:
new UltraGridBandLayout<MyModelClass>()
                .AddColumn("Unbound Column", configuration: c => c.CellActivation = Activation.NoEdit)
                .AddColumn(m => m.Description)
                .AddColumn(m => m.OtherColumn)
                //etc
                .AddDropDownListColumn(m => m.Aggregation, AggregationsList);

So using Resharper, I just went ahead and created the class structure and implemented the methods, I ended creating two objects, one is the UltraGridBandLayout, where I keep the order of things I've added, and a ColumnDefinition that is in charge of configuring a UltraGridColumn, this will allow me in the future to add new ColumnDefinition implementations if I need to add a Column stereotype different than the current one, like a ButtonColumnDefinition, etc.

here is the code for these two classes:

public class UltraGridBandLayout<T>
    {
        public IList<ColumnDefinition> Definitions { get; set; }

        public UltraGridBandLayout()
        {
            Definitions = new List<ColumnDefinition>();
        }

        public void ApplyTo(UltraGridBand band)
        {
            var columns = band.Columns.Cast<UltraGridColumn>();
            
            //hide all columns by default
            foreach (var column in columns)
                column.Hidden = true;
            
            for (int i = 0; i < Definitions.Count; i++)
            {
                var definition = Definitions[i];
                //find the column
                var column = columns.FirstOrDefault(c => c.Key == definition.Key);

                if (column == null) //if no bound column exists, then add an unbound one
                    column = band.Columns.Add(definition.Key);

                column.Header.VisiblePosition = i; //set the position where this column should be
                definition.ApplyTo(column); //apply extra settings 
            }
        }

        public UltraGridBandLayout<T> AddColumn(Expression<Func<T, object>> expression, string caption = null, Action<UltraGridColumn> configuration = null)
        {
            return AddColumn(expression.MemberName(), caption, configuration);
        }

        public UltraGridBandLayout<T> AddColumn(string key, string caption = null, Action<UltraGridColumn> configuration = null)
        {
            Definitions.Add(new ColumnDefinition(key, caption, configuration));
            return this;
        }

        public UltraGridBandLayout<T> AddDropDownListColumn(Expression<Func<T, object>> expression, IValueList list, string caption = null, Action<UltraGridColumn> configuration = null)
        {
            Action<UltraGridColumn> action = c =>
                                                 {
                                                     c.ValueList = list;
                                                     c.Style = ColumnStyle.DropDownList;
                                                 };


            return AddColumn(expression, caption, configuration == null
                                                      ? action
                                                      : c => { action(c); configuration(c); });
        }
    }

    public class ColumnDefinition
    {
        public string Key { get; set; }
        public string Caption { get; set; }
        public Action<UltraGridColumn> Configuration { get; set; }

        public ColumnDefinition(string key, string caption = null, Action<UltraGridColumn> configuration = null)
        {
            Key = key;
            Caption = caption ?? key;
            Configuration = configuration;
        }

        public void ApplyTo(UltraGridColumn column)
        {
            column.Header.Caption = Caption;
            column.Hidden = false;
            column.CellActivation = Activation.AllowEdit;

            if (Configuration != null)
                Configuration(column);
        }
    }


The idea here is that I can switch from this layout to other layout just by calling the ApplyTo method and passing the band I want to modify. Here is an example of the working solution:

public Form1()
        {
            #region dummy data
            Aggregations.Add(new AggregationModel() {Description = "Max", Id = 1});
            Aggregations.Add(new AggregationModel() {Description = "Min", Id = 2});
            Aggregations.Add(new AggregationModel() {Description = "Count", Id = 3});

            Operations.Add(new OperationModel() { Description = "Add", Id = 1});
            Operations.Add(new OperationModel() { Description = "Substract", Id = 2});
            Operations.Add(new OperationModel() { Description = "Multiply", Id = 3});
            Operations.Add(new OperationModel() { Description = "Divide", Id = 4});

            Data.Add(new RuleGroupModel()
                         {
                             Id = 1,
                             Description = "Rule Group 1",
                             Aggregation = Aggregations.ElementAt(2),
                             Groups = new List<RuleGroupModel>
                                          { new RuleGroupModel()
                                                  {
                                                      Id = 2,
                                                      Aggregation = Aggregations.ElementAt(1),
                                                      Description = "Child Rule Group 1",
                                                      Rules = new List<RuleModel>()
                                                                  {
                                                                      new RuleModel()
                                                                          {
                                                                              Id = 1,
                                                                              Name = "Rule 1",
                                                                              Operation = Operations.ElementAt(3)
                                                                          },
                                                                      new RuleModel()
                                                                          {
                                                                              Id = 1,
                                                                              Name = "Rule 2",
                                                                              Operation = Operations.ElementAt(1)
                                                                          },
                                                                      new RuleModel()
                                                                          {
                                                                              Id = 1,
                                                                              Name = "Rule 3",
                                                                              Operation = Operations.ElementAt(2)
                                                                          },
                                                                      new RuleModel()
                                                                          {
                                                                              Id = 1,
                                                                              Name = "Rule 4",
                                                                              Operation = Operations.ElementAt(3)
                                                                          }
                                                                  }
                                                  }
                                          }
                         });

            Data.Add(new RuleGroupModel() { Id = 3, Description = "Empty Group", Aggregation = null});
            #endregion

            InitializeComponent();

            bSetReadonly.Enabled = true;
            bSetEditable.Enabled = false;

            //create value lists for infragistics
            InitializeValueLists();
            //band layout initialization
            InitializeLayouts();

            ultraGrid1.DisplayLayout.MaxBandDepth = 5;
            ultraGrid1.InitializeLayout += InitializeLayout;
            ultraGrid1.DataSource = Data;
        }

        private void InitializeLayouts()
        {
            RuleBandLayout = new UltraGridBandLayout<RuleModel>() //not editable band
                .AddColumn(m => m.Name, configuration: c => c.CellActivation = Activation.NoEdit)
                .AddDropDownListColumn(m => m.Operation, OperationsList, configuration: c => c.CellActivation = Activation.NoEdit);

            RuleGroupBandLayout = new UltraGridBandLayout<RuleGroupModel>()
                .AddColumn(m => m.Description)
                .AddDropDownListColumn(m => m.Aggregation, AggregationsList);
        }

        private void InitializeValueLists()
        {
            OperationsList = new ValueList();
            foreach (var operation in Operations)
                OperationsList.ValueListItems.Add(operation, operation.Description);

            AggregationsList = new ValueList();
            foreach (var aggregation in Aggregations)
                AggregationsList.ValueListItems.Add(aggregation, aggregation.Description);
        }


        void InitializeLayout(object sender, InitializeLayoutEventArgs e)
        {
            SetLayout(e.Layout);
        }

        private void SetLayout(UltraGridLayout layout)
        {
            var bands = layout.Bands.Cast<UltraGridBand>();
            
            //apply our layout to the auto generated bands
            foreach (var band in bands)
                switch (band.Key)
                {
                    case "Rules":
                        RuleBandLayout.ApplyTo(band);
                        break;
                    default:
                        RuleGroupBandLayout.ApplyTo(band);
                        break;
                }
        }

        private void bSetReadonly_Click(object sender, EventArgs e)
        {
            var columns = ultraGrid1.DisplayLayout.Bands.Cast<UltraGridBand>().SelectMany(b => b.Columns.Cast<UltraGridColumn>());

            foreach (var column in columns)
                column.CellActivation = Activation.NoEdit;

            bSetReadonly.Enabled = false;
            bSetEditable.Enabled = true;
        }

        private void bSetEditable_Click(object sender, EventArgs e)
        {
            SetLayout(ultraGrid1.DisplayLayout);
            bSetReadonly.Enabled = true;
            bSetEditable.Enabled = false;
        }

Here is the download link if anyone would like to see it working; bear in mind that you will need the infragistics dlls installed for it to run!

No comments:

Post a Comment