Creating TreeView using Grid in Silverlight (Component One – Cell Factory) – III

[Component one already have a new attribute in C1FlexGrid ‘ ChildItemsPath=”Children” which will create the tree view look and feel without any of this custom cell factory. I did it anyway as a means to learn]

In the previous blog post, we took our first step in creating tree view look and feel using C1FlexGrid. With some basic changes we were able to get to the result close enough.

image

Few things missing for the grid to look like tree view, they are

1. When parent do not have a child, the expand and collapse icon should not appear.

2. We are missing check box along with parent id.

We can resolve both the problems using Cell Factory.  What does cell factory do? Cell Factory is used by C1FlexGrid to draw cells. It has bunch of methods that you can override so that you can implement your own drawing mechanism. In our case, when it create the cell content, I need to check if the content is a group row, then add my own cell creating logic put check box else let the grid do its work. So we create our custom class off of CellFactory. All the code you are going to see below is stripped down version of Component One’s iTune sample code.

public class MyCellFactory:CellFactory
{
}

The method we need to override to achieve custom cell content create is ‘CreateCellContent’ method. In this method, we will check, if the cell belongs to a group row and if it is first column then create me the custom cell.

static Thickness _emptyThickness = new Thickness(0);

//bind a cell to a range
 public override void CreateCellContent(C1FlexGrid grid, Border bdr, CellRange range)
{
     var row = grid.Rows[range.Row];
     var col = grid.Columns[range.Column];
      var gr = row as GroupRow;

      if (gr != null)
          bdr.BorderThickness = _emptyThickness;

      if (gr != null && range.Column == 0)
     {
          BindGroupRowCell(grid, bdr, range);
          return;
      }
      base.CreateCellContent(grid, bdr, range);
}


One point of interest in the above method call is ‘BindGroupRowCell’. This method will create the custom cell for us. This has two important parts in it, first one is to make sure the group row has a databinding source and also create custom cell and add it to its cell content.

private void BindGroupRowCell(C1FlexGrid grid, Border bdr, CellRange range)
{
     var row = grid.Rows[range.Row];
     var gr = row as GroupRow;

     if (range.Column == 0)
     {
          if (gr.DataItem == null)
         {
              gr.DataItem = BuildGroupDataItem(gr);
         }

        Type cellType = typeof(ParentCell);
        bdr.Child = (ImageCell)new ParentCell(row);

     }
}

In our grid, every row has an associated data row except the group row. Since group row does not have data item, we will, create an temporary data row, that we can use to bind it to the grid. When we group the rows to generate the grouping in data grid, the group knows about all the children of the group. We conveniently pick the first row and use the data to generate the dummy row that we use to bind it to group row.

CustomCellFactoryForTreeView.MainPage.Person BuildGroupDataItem(GroupRow gr)
{
     var gs = gr.GetDataItems().OfType<CustomCellFactoryForTreeView.MainPage.Person>();
     CustomCellFactoryForTreeView.MainPage.Person p = new CustomCellFactoryForTreeView.MainPage.Person();
      if (gs != null && gs.Count() > 0)
            p = gs.ElementAt(0) as CustomCellFactoryForTreeView.MainPage.Person;
       return new CustomCellFactoryForTreeView.MainPage.Person()
      {
            ParentID = p.ParentID,
            Description = p.Description,
            ChildID = p.ChildID,
            ChildDescription = p.ChildDescription
       };
 }

Now lets look at the main core functionality to  create the check box and text in the group row. Again following code is purely stripped down version itunes sample in C1 samples.  Lets first create the ParentCell

    public  class ParentCell : ImageCell
    {
        const double ALPHA = 0.5;
        GroupRow _gr;
        Image _nodeImage;
        static ImageSource _bmpExpanded, _bmpCollapsed;

        public ParentCell(Row row)
            : base(row)
        {
            CustomCellFactoryForTreeView.MainPage.Person per = row.DataItem as CustomCellFactoryForTreeView.MainPage.Person;
            if (per != null && per.ChildID != null)
            {
                // create collapsed/expanded images
                if (_bmpExpanded == null)
                {
                    _bmpExpanded = ImageCell.GetImageSource("Expanded.png");
                    _bmpCollapsed = ImageCell.GetImageSource("Collapsed.png");
                }

                // store reference to row
                _gr = row as GroupRow;

                // initialize collapsed/expanded image
                _nodeImage = new Image();
                _nodeImage.Source = _gr.IsCollapsed ? _bmpCollapsed : _bmpExpanded;
                _nodeImage.Width = _nodeImage.Height = 9;
                _nodeImage.VerticalAlignment = VerticalAlignment.Center;
                _nodeImage.Stretch = Stretch.None;
                _nodeImage.MouseLeftButtonDown += img_MouseLeftButtonDown;
                _nodeImage.MouseEnter += img_MouseEnter;
                _nodeImage.MouseLeave += img_MouseLeave;
                _nodeImage.Opacity = ALPHA;
                Children.Insert(0, _nodeImage);
            }
            // make text bold
            TextBlock.FontWeight = FontWeights.Bold;
        }
        void img_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            var img = sender as Image;
            var cell = img.Parent as NodeCell;
            cell.IsCollapsed = !cell.IsCollapsed;
        }
        void img_MouseEnter(object sender, MouseEventArgs e)
        {
            var img = sender as Image;
            img.Opacity = 1;
        }
        void img_MouseLeave(object sender, MouseEventArgs e)
        {
            var img = sender as Image;
            img.Opacity = ALPHA;
        }
        public override Row Row
        {
            set
            {
                // update image
                _gr = value as GroupRow;
                _nodeImage.Source = _gr.IsCollapsed ? _bmpCollapsed : _bmpExpanded;
                _nodeImage.Opacity = ALPHA;

                // update text
                base.Row = value;
            }
        }
        public bool IsCollapsed
        {
            get { return _gr.IsCollapsed; }
            set
            {
                _gr.IsCollapsed = value;
                _nodeImage.Source = value ? _bmpCollapsed : _bmpExpanded;
            }
        }
    }

The point of interest in here is the constructor, where we check if there are any children available. If there is any child available, then we show expand or collapse icon. If there are no children then do not show any icon. We also add event handler which listens to the click event on the image and based on the event, it toggles the state. With this change, we took care the icon to show only when there is children. One thing left to do is, adding check box to the control. This is accomplished at base class ‘ImageCell’.

The Image cell is derived from StackPanel so it is easy to customize it to the look you want.

    public abstract class ImageCell:StackPanel
    {
        public ImageCell(Row row)
        {
            Orientation = System.Windows.Controls.Orientation.Horizontal;

            CheckBox box = new CheckBox();
            box.VerticalAlignment = System.Windows.VerticalAlignment.Center;
            box.Click += new RoutedEventHandler(box_Click);
            Children.Add(box);

            TextBlock tb = new TextBlock();
            tb.VerticalAlignment = System.Windows.VerticalAlignment.Center;
            Children.Add(tb);

            BindCell(row.DataItem);
        }

        void box_Click(object sender, RoutedEventArgs e)
        {
            int k = 0;
        }

        public TextBlock TextBlock
        {
            get { return Children[Children.Count - 1] as TextBlock; }
        }

        public CheckBox CheckBox
        {
            get
            {
                return Children[Children.Count - 2] as CheckBox;
            }
        }

        public virtual Row Row
        {
            set { BindCell(value.DataItem); }
        }

        private void BindCell(object dataItem)
        {
            var binding = new Binding("Description");
            binding.Source = dataItem;
            TextBlock.SetBinding(TextBlock.TextProperty, binding);
            var cbbinding = new Binding("TwoState");
            cbbinding.Source = dataItem;
            CheckBox.SetBinding(CheckBox.IsCheckedProperty, cbbinding);
        }

        public static ImageSource GetImageSource(string imageName)
        {
            var bmp = new BitmapImage();
            bmp.CreateOptions = BitmapCreateOptions.None;
            var fmt = "Resources/{0}";
            bmp.UriSource = new Uri(string.Format(fmt, imageName), UriKind.Relative);
            return bmp;
        }
    }

The final result of course is like the following

image

Our requirement is to have a check box and a text block in the group row and that is what we did in the constructor of the ImageCell. Once it is created we need to bind it to the proper fields. That’s is what done in the BindCell method. One thing, it is important here on how you get to the control for doing the binding. Please see the TextBlock getter property, which in turn take the first child from the children collection and return it for binding. If you remember the children have three elements and they are in the following order. First element (at 0) is image for collapse and expand. Second one is the check box (at 1) and third one is the TextBlock (at 2). I was so lazy to I hard coded it to get the elements by hard coded index. You can traverse and identify the control to make it flexible if you want to move around the control. One another thing, you may want is to listen to the check box click event that you can listen with click event.

I would like to thank Bernardo @ Component One to take time to answer my questions and excellent example of itunes on their web site. This three part blog was more for me to understand how the whole components works together and what you can do with this. You can customize the grid any way you want. In this whole example we looked at only one method, create cell, but there are five or six more methods available completely change the look of the grid.

Creating TreeView using Grid in Silverlight (Component One – Cell Factory) – I

[Component one already have a new attribute in C1FlexGrid ‘ ChildItemsPath="Children" which will create the tree view look and feel without any of this custom cell factory. I did it anyway as a means to learn.]

Silverlight 4 tree view have one performance problem, when you try to expand a node with 1000 odd children, it takes close to 10 to 14 second to load. But it is only the initial cost, once the data loaded into memory, when you expand or collapse the same node it expands quickly. If you have many number of nodes with 1000s of children then you are going incur the cost of each node expansion. As you can imagine it does not go very well with the end users. For starters, it will not go past QA when they see that bad of performance. So I was thinking about adopting data grid to display the same content, yet make it look like tree view. The reason I chose grid instead of any other controls is virtualization. I tested a simple grid with same amount of data in it and it was simply amazing. It was so fast. You can’t time it since it expand and collapse instantly.

I have been using Component One control lately for our development and I very comfortable using it. I decided to develop the grid using C1FlexGrid and one of its power feature called cell factory. I never really appreciated the full power of  till I attempted to solve my problem using cell factory. I decided to blog about it as a document of this feature and as a reference for future. The best reference if you want to learn how the cell factory can be used, I would recommend you to get to Component One web site and look for Silverlight FlexGrid samples. There is also a PDF documentation explaining in detail how to use cell factory with an iTunes like grid.

Ok, that is enough talk, lets see the problem at hand. Let me first show, what am I trying to solve and then we see the code.

image

image

The first picture shows the parents collapsed and the second picture shows the first parent expanded. To make this exercise simple, I have only two level. There are 1000 parents and every even parent have 500 children. Lets look at the class definition for the collection. The class is called Person and shown as follows

 1: public class Person:INotifyPropertyChanged
 2: {
 3:     public string Name { get; set; }
 4:     public int Age { get; set; }
 5:     public bool TwoState { get; set; }
 6:     public ObservableCollection<Person> Children { get; set; }
 7:    8:     public Person()
 9:     {
 10:         TwoState = false;
 11:         Children = new ObservableCollection<Person>();
 12:     }
 13:    14:     public event PropertyChangedEventHandler PropertyChanged;
 15:     private void NotifyPropertyChanged(string info)
 16:     {
 17:         if (PropertyChanged != null)
 18:        {
 19:             PropertyChanged(this, new PropertyChangedEventArgs(info));
 20:        }
 21:     }
 22: }

Following code load the data to be displayed on the treeview

 1: private void LoadPeople()
 2: {
 3:      _people = new List<Person>();
 4:      for (int i = 0; i < 1000; i++)
 5:      {
 6:           Person p1;
 7:           if (i % 2 == 0)
 8:          {
 9:               p1 = new Person() { Name = "parent" +i.ToString(), Age = 20 };
 10:               for (int j = 0; j < 500; j++)
 11:                  p1.Children.Add(new Person() { Name = "Child" + j.ToString(), Age = 10 });
 12:          }
 13:          else
 14:          {
 15:              p1= new Person() { Name = "Parent" + i.ToString(), Age = 20 };
 16:          }
 17:          _people.Add(p1);
 18:     }
 19: }

As one of my friend pointed out, it is better to use string.format rather than ‘+’ in string operations. Now lets look at the XAML which does the actual work of showing the data

 1: <controls:TreeView Grid.Column="1" VerticalAlignment="Stretch" VirtualizingStackPanel.VirtualizationMode="Standard"
 2:                                    ItemsSource="{Binding People}" >
 3:      <controls:TreeView.ItemTemplate>
 4:           <common:HierarchicalDataTemplate ItemsSource="{Binding Children}">
 5:                 <StackPanel>
 6:                     <Grid>
 7:                         <Grid.ColumnDefinitions>
 8:                             <ColumnDefinition Width="10*"/>
 9:                             <ColumnDefinition Width="90*"/>
 10:                          </Grid.ColumnDefinitions>
 11:                          <CheckBox IsChecked="{Binding TwoState}" Grid.Column="0"/>
 12:                             <TextBlock Grid.Column="1" Text="{Binding Name}"/>
 13:                       </Grid>
 14:                  </StackPanel>
 15:            </common:HierarchicalDataTemplate>
 16:        </controls:TreeView.ItemTemplate>
 17: </controls:TreeView>

With these three set of code, I am able to generate the tree view using toolkit tree view control. When you run the code initial load takes almost 30 to 35 seconds to load and then each child expand and collapse takes around 10 to 15 seconds to load. Once children are loaded in memory, future expand and collapse are very fast. The main problems are initial load and time to expand each node.

Now we know the problem at hand, we can look at how we can use C1FlexGrid to create tree view look and feel.

How to use another control in UI as part of a control’s binding?

Lets say that you want to bind data based on one UI control, how would we go about doing it? I attempted one solution. First lets see how the output look like

image

As you can see it has a grid with 10 data items. At the bottom there is a label and text block, the text block shows how many items are in the grid. Also there is a filter button. Every time you press filter button, it will filter all the items in age increment of 20. So if I press filter then the result would be like the following

image

There is not data since the filter will look for age < 20. Now filter again

image

Now there will be 2 items,  where age is less than 40 and so on. The point we are interested in is the Total rows, as the number of rows changes the grid, the number also changes. Lets look the XAML how it is done.

XAML:

 1: <Grid x:Name="LayoutRoot" Background="White">
 2:   <sdk:DataGrid AutoGenerateColumns="true" Height="156" HorizontalAlignment="Left" Margin="35,24,0,0" Name="dataGrid1" VerticalAlignment="Top" Width="285" />
 3:   <TextBlock Height="23" HorizontalAlignment="Left" Margin="190,214,0,0" Name="textBlock1" Text="{Binding ElementName=dataGrid1, Path=ItemsSource.Count}" VerticalAlignment="Top" Width="96" />
 4:   <Button Content="Filter" Height="23" HorizontalAlignment="Left" Margin="313,214,0,0" Name="Button" VerticalAlignment="Top" Width="75" Click="button1_Click" />
 5:   <sdk:Label Height="28" HorizontalAlignment="Left" Margin="35,209,0,0" Name="label1" Content="Total Rows" VerticalAlignment="Top" Width="133" />
 6: </Grid>

The line we are interested in is line (3), look at the Text binding for text block. Here we say, we are want the source of data binding will be the data grid and, I am interested in total count of items source.

Lets look at the code behind on how the data binding happens and how filtering happens.

Code behind:

public partial class MainPage : UserControl
{
    PagedCollectionView peopleCollection;
    List<Person> people;
    private int personNumber=1;

    public MainPage()
    {
        InitializeComponent();
        people = new List<Person>();
        for(int i=0;i<10;i++)
            people.Add(new Person() { Name = i.ToString(), Age = 20 + i * 10 });
        peopleCollection = new PagedCollectionView(people);
        dataGrid1.ItemsSource = peopleCollection;
    }

    private void button1_Click(object sender, RoutedEventArgs e)
    {
        peopleCollection.Filter = null;
        peopleCollection.Filter = (p => ((Person)p).Age < (personNumber * 20));
        personNumber++;
    }
}

The code is simple and straight forward. I create a person collection and then converted it to PagedCollectionView and bind it to the grid. When user clicks Filter button, I clear the filter first then apply filter. There is no code in code behind which changes the text block count. It is all done in the XAML with stright control binding.

Digg This

Making two cells in a grid dependable

One of my colleague was trying to create a Silverlight grid, where two columns are dependent. For sake of simplicity, lets say I have two columns, A and B. If A has a value in a valid range then Column B will be editable otherwise it is read only. Even though I said, it is columns, in reality it is cells. The reason I did not make the distinction in the beginning is that, it is taken care by binding. So how do we go about solve this problem. It is actually very easy than what we think. Now that we going to see the example, just want to point one out, I use Component One data gird a lot and I thought, I will bring in component one grid in this example as well.

1. First create a class where we will use this dependency. For simplicity I created a Person class as follows.

public class Person : INotifyPropertyChanged
{
   private string _name;
   public string Name
   {
      get
      {
         return _name;
       }
       set
       {
          _name = value;
          RaisePropertyChanged("Name");
       }
   }

   private double _amount;
   private int _bonus;

   public double Amount
   {
        get
        {
            return _amount;
        }
        set
        {
            _amount = value;
            RaisePropertyChanged("Amount");
         }
    }

    public int Bonus
    {
        get
        {
            return _bonus;
         }
         set
         {
             _bonus = value;
             RaisePropertyChanged("Bonus");
         }
     }

     public void RaisePropertyChanged(string propertyName)
     {
         if (PropertyChanged != null)
         {
              PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
          }
     }

     public event PropertyChangedEventHandler PropertyChanged;
}

In the above class, the two fields that we are interested in are Amount and Bonus. When the amount is more than 1000 then bonus field will become editable.

2. Create XAML to bind the collection as follows

 1:     <Grid x:Name="LayoutRoot" Background="White">
 2:         <Grid.Resources>
 3:             <converter:ColorConverterForReadOnly x:Key="converter1"/>
 4:             <converter:ReadOnlyConverter x:Key="converter2"/>
 5:         </Grid.Resources>
 6:         <c1:C1FlexGridExcel Name="_flex" AutoGenerateColumns="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
 7:             <c1:C1FlexGridExcel.Columns>
 8:                 <c1:Column Binding="{Binding Name}"/>
 9:                 <c1:Column Binding="{Binding Age}"/>
 10:                 <c1:Column Binding="{Binding Amount, Mode=TwoWay}"/>
 11:                 <c1:Column Binding="{Binding Bonus, Mode=TwoWay}" HorizontalAlignment="Right">
 12:                     <c1:Column.CellTemplate>
 13:                         <DataTemplate>
 14:                             <Grid>
 15:                                 <Border Background="{Binding Amount, Converter={StaticResource converter1}}"/>
 16:                                 <TextBlock Text="{Binding Bonus}" VerticalAlignment="Center" HorizontalAlignment="Right"/>
 17:                             </Grid>
 18:                         </DataTemplate>
 19:                     </c1:Column.CellTemplate>
 20:                     <c1:Column.CellEditingTemplate>
 21:                         <DataTemplate>
 22:                             <Grid>
 23:                                 <Border Background="{Binding Amount, Converter={StaticResource converter1}}"/>
 24:                                 <TextBox IsReadOnly="{Binding Amount, Converter={StaticResource converter2}}" Text="{Binding Bonus, Mode=TwoWay}" 
 25:                                      Background="{Binding Amount, Converter={StaticResource converter1}}" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0"/>
 26:                             </Grid>
 27:                         </DataTemplate>
 28:                     </c1:Column.CellEditingTemplate>
 29:                 </c1:Column>
 30:             </c1:C1FlexGridExcel.Columns>
 31:         </c1:C1FlexGridExcel>
 32:     </Grid>

In the above code,you will notice I am using two converters. First converter will set the back ground color to readonly background color when the amount is less than 1000. The second converter will return true or false based on amount field.

So if you would notice, from line 12 – 28, have two definitions. one for cell template to render the data and cell edit template which will be used when user try to edit the cell.

In cell template, line 15, will set the back ground of the cell to read only look based on the amount. As you can see, Background is bound to amount but the brush will be set in the converter. (will see the converter later).

Line 16 basically shows a text block which by default readonly to show the data.

Line 23 is same as line 15 to change the background color.

Line 24 is a text box which will allow the user to edit the cell if the amount is greater than 1000. This is achived by IsReadOnly property, which is bound to Amount again, when it is passed through the converter, it will check and see if the amount is greater than 1000 and based on the value, it will return true or false. So if the amount is less than 1000 then isreadonly will be true thus making the cell readonly.

3. The Converters:

 1: public class ColorConverter : IValueConverter
 2:     {
 3:         public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
 4:         {
 5:             double? data = value as double?;
 6:  
 7:             if (data == null || data < 1000)
 8:             {
 9:                 return new SolidColorBrush(Color.FromArgb(255, 200, 200, 200));
 10:             }
 11:             else
 12:                 return null;
 13:         }
 14:  
 15:         public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
 16:         {
 17:             return null;
 18:         }
 19:     }
 20:  
 21:     public class ReadOnlyConverter : IValueConverter
 22:     {
 23:         public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
 24:         {
 25:             double? data = value as double?;
 26:  
 27:             return (data == null || data < 1000);
 28:         }
 29:  
 30:         public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
 31:         {
 32:             return value;
 33:         }
 34:     }

The first converter is background color converter called ColorConverter. Which is used in the background property binding. As you can see in line 7, when the amount is less than 1000, then it returns gray background brush. Other wise, it returns null.

The second converter (line 21) is read only converter. This will return true, when the amount is less than 1000 otherwise is false at line 27.

How to order columns while binding to Grid?

One of my silverlight newbie friend asked, when I have a collection of data that I bind to grid and want to autogenerate columns based on the class it is bound to but I want to control the order it appear and also I do not want to display some of the columns. It was funny when he asked me I was stumped since I never did this kind of binding. Well, it is not that difficult. ComponentModel is here to help. So here is how we do it.

1. Create you XAML which which will have nothing but a Grid like the following

   1:  <sdk:DataGrid Name="_normal" AutoGenerateColumns="True" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="0">
   2:  </sdk:DataGrid>  
 
2. Create your model. In my case I created a person class. 
public class Person: IDataErrorInfo
{
     [StringLength(20)]
     [Display(Description="Name", Order=1)]
     public string Name { get; set; }

     [Range(0, 100)]
     [Display(Description="Age", Order=2)]
     public int Age { get; set; }

     [Display(Description="Address 1", Order=5)]
     public string Address1 { get; set; }

     [Display(Description = "Address 2", Order = 4)]
     public string Address2 { get; set; }

     [Display(Description="Amounts", Order=3)]
     public double Amount {get; set; }

     [Display(AutoGenerateField=false)]
     public double DoNotShowThisField {get; set; }

     private double doNotShowThisField1;     
}

Now you create your data collection and bind it to your gird. Your output would be something like the following

image

The attribute, Display sets the column order to display in the grid. As you can see Amount comes last in the class property but the Display order was set to 3 so it appears at 3rd column. Also notice DoNotShowThisField, even though it is declared as public, the display attribute says, when autogenerate fields for data binding, do not include this field.

Hope this helps someone.