Showing posts with label DataGrid. Show all posts
Showing posts with label DataGrid. Show all posts

Thursday, October 22, 2009

Silverlight Datagrid Layout Manager: The code

In the previous post I talked about a datagrid layout manager we used in our product. There is a demo and a link to code in that post as well. By the way, one thing I forgot to mention in that post: A user can drag/drop to rearrange the columns, and then if she opens "manage layout" and hit "save", the rearranged layout will persist to a future session. An added bonus.

Here is the the full demo, including user control and consuming code: http://www.filefactory.com/file/a06da5d/n/ContextMenuDemo_zip

I will try to break down the code in this post.

The column information in the datagrid is massaged into an ObservableCollection object. The definition for OrderedItem:



   1:      public class OrderedItem : INotifyPropertyChanged
   2:      {
   3:          public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
   4:   
   5:          private string _Name;
   6:          public string Name
   7:          {
   8:              get { return _Name; }
   9:              set
  10:              {
  11:                  _Name = value;
  12:                  if (PropertyChanged != null)
  13:                  {
  14:                      PropertyChanged(this, new PropertyChangedEventArgs("Name"));
  15:                  }
  16:              }
  17:          }
  18:   
  19:          public string DisplayName { get; set; }
  20:          private int _Position;
  21:          public int Position
  22:          {
  23:              get { return _Position; }
  24:              set
  25:              {
  26:                  _Position = value;
  27:                  if (PropertyChanged != null)
  28:                  {
  29:                      PropertyChanged(this, new PropertyChangedEventArgs("Position"));
  30:                  }
  31:              }
  32:          }
  33:   
  34:          private bool _IsFrozen;
  35:          public bool IsFrozen
  36:          {
  37:              get { return _IsFrozen; }
  38:              set
  39:              {
  40:                  _IsFrozen = value;
  41:                  if (PropertyChanged != null)
  42:                  {
  43:                      PropertyChanged(this, new PropertyChangedEventArgs("IsFrozen"));
  44:                  }
  45:              }
  46:          }
  47:   
  48:          private bool _IsIncluded;
  49:          public bool IsIncluded
  50:          {
  51:              get { return _IsIncluded; }
  52:              set
  53:              {
  54:                  _IsIncluded = value;
  55:                  if (PropertyChanged != null)
  56:                  {
  57:                      PropertyChanged(this, new PropertyChangedEventArgs("IsIncluded"));
  58:                  }
  59:              }
  60:          }
  61:   
  62:          public OrderedItem Copy()
  63:          {
  64:              return new OrderedItem
  65:              {
  66:                  DisplayName = DisplayName,
  67:                  IsIncluded = IsIncluded,
  68:                  Name = Name,
  69:                  Position = Position
  70:              };
  71:          }
  72:   
  73:      }

This method examines the datagrid and creates the ObservableCollection:



   1:  public void ShowLayoutDialog()
   2:  {
   3:  LayoutRoot.Visibility = Visibility.Visible;
   4:  List<ordereditem> ls = new List<ordereditem>();
   5:  for (int i = ZeroBasedStartIndex; i < GridToFormat.Columns.Count; i++)
   6:              {
   7:                  var oi = new OrderedItem
   8:                  {
   9:                      Name = GridToFormat.Columns[i].Header.ToString(),
  10:                      Position = GridToFormat.Columns[i].DisplayIndex + 1 - ZeroBasedStartIndex,
  11:                      IsIncluded = (GridToFormat.Columns[i].Visibility == Visibility.Visible),
  12:                      IsFrozen = GridToFormat.Columns[i].IsFrozen
  13:                  };
  14:                  oi.PropertyChanged += new PropertyChangedEventHandler(OrderedItem_PropertyChanged);
  15:                  ls.Add(oi);
  16:              }
  17:              ls.Sort(new OrderedItemComparer());
  18:              ChooserGrid.ItemsSource = ls.ToObservableCollection<OrderedItem>();
  19:  }

Here is the method that applies the layout to the grid. InitLayout() and the Save-click handler method calls this method to do their heavy lifting.



   1:          private void UpdateGridLayout(OrderedItem[] layouts)
   2:          {
   3:              try
   4:              {
   5:                  if (layouts != null)
   6:                  {
   7:                      int fc = (from l in layouts where l.IsFrozen select 1).Count();
   8:                      GridToFormat.FrozenColumnCount = (fc > 0) ? fc + ZeroBasedStartIndex : 0;
   9:                      foreach (OrderedItem layout in layouts)
  10:                      {
  11:                          foreach (DataGridColumn c in GridToFormat.Columns)
  12:                          {
  13:                              if (c.Header.ToString() == layout.Name)
  14:                              {
  15:                                  c.Visibility = layout.IsIncluded ? Visibility.Visible : Visibility.Collapsed;
  16:                                  c.DisplayIndex = layout.Position - 1 + ZeroBasedStartIndex;
  17:                                  break;
  18:                              }
  19:                          }
  20:                      }
  21:                  }
  22:                  else
  23:                  {
  24:                      for (int i = ZeroBasedStartIndex; i < GridToFormat.Columns.Count; i++)
  25:                      {
  26:                          GridToFormat.Columns[i].DisplayIndex = i;
  27:                          GridToFormat.Columns[i].Visibility = Visibility.Visible;
  28:                      }
  29:                  }
  30:              }
  31:              catch
  32:              {
  33:                  UpdateGridLayout(null);
  34:                  _AppSettings[AppKey] = null;
  35:                  _AppSettings.Save();
  36:              }
  37:          }

There are other methods that handle PropertyChanged event for the OrderedItem, and to handle user actions to movver the item up or down. I just want to point out what each method did, and I think the xaml and cs code (that can be downloaded from link in previous post) are pretty self-explanatory. I hope this gives you enough info about the the guts of the user-control to tweak it to your liking.

Wednesday, October 14, 2009

Silverlight DataGrid: Letting Users Manage Layout

We have a few datagrids that contain a lot of columns, and different columns are important to different users. They want to see only a subset of what is available, and use different fields as keys to the dataset. So, we had to come up with a way for the users to:
1. Rearrrange the columns
2. Make columns visibile/invisible
3. Freeze some columns in the beginning (keys) so they remain visible during horizontal scrolling.
4. Carry over the user preferences to future sessions.

We managed to come up with a generic user control to do this for any datagrid, and I think it is worth sharing here. Here is an application that demonstrates the capability:



It is just the same datagrid from previous posts, with some extraneous fields to play around with. You get the idea.

The code for the user control can be found here

To use the DataGridLayoutChooser, you would write some xaml like this:


   1:  <lcl:DataGridLayoutChooser Name="dgLayoutChooser" AppKey="dgLayout" Grid.RowSpan="2"/>
The property AppKey is the key that it uses to save the preferences in isolated storage, for use in future sessions.

When "Loaded" event handler for your app/usercontrol, you want to specify the DataGrid that the Chooser is formatting:


   1:  void Home_Loaded(object sender, RoutedEventArgs e)
   2:  {
   3:      dgLayoutChooser.GridToFormat = dg;
   4:      dgLayoutChooser.InitLayout();
   5:  }
InitLayout() applies any layout that the user may have saved from a previous session.
Now, when the user wants to manage the layout (Click event in the demo, for instance), you call the ShowLayoutDialog() method on the Chooser:


   1:  void ManageLayout(object sender, RoutedEventArgs e)
   2:  {
   3:      dgLayoutChooser.ShowLayoutDialog();
   4:  }
We will take a deeper look at the user-control code in the next post. The DataGridLayoutChooser is a very good candidate to be made into a real control, by the way. I will make that available in a few weeks, when I get around to it. In the meantime, I hope you find this UserControl as useful as we did.

Friday, October 9, 2009

Copying Datagrid data to Clipboard through Context Menu

In my last post, I talked about adding context menu to Silverlight DataGrid. One thing that always frustrated our users was that they could not copy contents of a datagrid in our application to the clipboard.

Here is the example (clipboard is accessible through javascript only in IE, mind you):



I will make the full code available on Monday. Below, is a runthrough of how this was done.

There are plenty of examples on how to access the clipboard in Silverlight through javascript (for IE, anyways). Here is the code that I used, from Jeff Wilcox's example:


1:          public static void SetText(string text)
2:          {
3:              var clipboardData = (ScriptObject)HtmlPage.Window.GetProperty("clipboardData");
4:              if (clipboardData != null)
5:              {
6:                  bool success = (bool)clipboardData.Invoke("setData", "text", text);
7:                  if (!success)
8:                  {
9:                      HtmlPage.Window.Alert(ClipboardFailure);
10:                  }
11:              }
12:              else
13:              {
14:                  HtmlPage.Window.Alert("clipboard not available");
15:              }
16:          }

You can always ctrl+C to copy contents of a textbox or textblock to clipboard, but DataGrid does not allow that. To do this, I take the cell in context, loop through it's children recursively, and write out the contents of the child if it is TextBlock or TextBox:



1:          private static string GetText(UIElement uie)
2:          {
3:              string ret = string.Empty;
4:              if (uie != null)
5:              {
6:                  if (uie is TextBlock)
7:                      ret = (uie as TextBlock).Text;
8:                  else if (uie is TextBox)
9:                      ret = (uie as TextBox).Text;
10:                  else if (uie is Panel)
11:                  {
12:                      foreach (var element in (uie as Panel).Children)
13:                          ret += GetText(element);
14:                  }
15:              }
16:              return ret;
17:          }


Now, what if we want to have a menu command that copies whole row data? I have a generic serializer that takes the datacontext of the row as object and, using reflection, recursively creates a string representation:



   1:          private static string Serialize(object data)
   2:          {
   3:              if (data != null)
   4:              {
   5:                  var sb = new StringBuilder();
   6:                  var props = data.GetType().GetProperties();
   7:                  sb.Append(data.GetType().Name);
   8:                  foreach (var prop in props)
   9:                  {
  10:                      var o = prop.GetValue(data, null);
  11:                      sb.Append(Environment.NewLine).Append(prop.Name).Append(" = ");
  12:                      if (o == null || o is string || o is DateTime || o.GetType().IsPrimitive)
  13:                          sb.Append(o);
  14:                      else
  15:                          sb.Append(Serialize(o).Replace(Environment.NewLine, Environment.NewLine + "\t"));
  16:                  }
  17:                  return sb.ToString();
  18:              }
  19:              else return string.Empty;
  20:          }

How do we get to a reference to the cell? When context menu is requested, the DataGrid finds the cell where the right-click occurred, and keeps a reference to in a private field, to be used when the menu selection occurs. So the GetContextMenuContent method changes like so:



   1:          public virtual List<ContextMenuItemInfo> GetContextMenuContent(System.Windows.Browser.HtmlEventArgs e)
   2:          {
   3:              Point curr = new Point(e.OffsetX, e.OffsetY);
   4:              _CellInContext = null;
   5:              for (var i = 0; i < presenter.Children.Count; i++)
   6:              {
   7:                  var row = (DataGridRow)presenter.Children[i];
   8:                  if (ContextMenuInterceptor.IsPointWithinBounds(curr, row))
   9:                  {
  10:                      foreach (var col in Columns)
  11:                      {
  12:                          var cell = col.GetCellContent(row.DataContext).Parent as DataGridCell;
  13:                          if (cell != null && ContextMenuInterceptor.IsPointWithinBounds(curr, cell))
  14:                          {
  15:                              _CellInContext = cell;
  16:                              break;
  17:                          }
  18:                      }
  19:   
  20:                      foreach (ContextMenuItemInfo m in ContextMenuList)
  21:                          m.Tag = row;
  22:                      return ContextMenuList;
  23:                  }
  24:              }
  25:              return null;
  26:          }

And the HandleContextMenu actually handles some stuff instead of just raising an event:



   1:          public virtual void HandleContextMenuClick(object sender, ContextMenuClickEventArgs e)
   2:          {
   3:              switch (e.ClickedItem.Key)
   4:              {
   5:                  case CMD_COPY_ROW:
   6:                      Clipboard.SetText(Serialize(((DataGridRow)e.ClickedItem.Value).DataContext));
   7:                      break;
   8:                  case CMD_COPY_CELL:
   9:                      if (_CellInContext != null)
  10:                          Clipboard.SetText(GetText(_CellInContext.Content as UIElement));
  11:                      break;
  12:                  default:
  13:                      if (ContextMenuClicked != null)
  14:                          ContextMenuClicked(sender, e);
  15:                      break;
  16:              }
  17:          }

Both of these have worked very well for our application. Both "Copy Cell Content" and "Copy Row Data" are context menu commands in all our datagrids.

Sunday, September 27, 2009

Silverlight DataGrid with Row-Aware Context Menu

 In my last post I talked about adding context-menus to controls. DataGrid is a control that we use in most projects, and it will be nice to have this context menu be aware of the row on which the user clicked and act on the datacontext for that row. Here is a sample application that demonstrates such bahavior:



The code can be found here.

We are creating the context menu and handling right-clicks the same way as described in my earlier post. The main thing is to create a DataGrid which can tell you the row that is clicked. I found that if you inherit from DataGrid, you have access to the RowsPresenter.


   1:  protected const string PRESENTER_CHILD_NAME = "RowsPresenter";
   2:  protected DataGridRowsPresenter presenter;
   3:   
   4:  public override void OnApplyTemplate()
   5:  {
   6:     base.OnApplyTemplate();
   7:     presenter = (DataGridRowsPresenter)GetTemplateChild(PRESENTER_CHILD_NAME);
   8:  }

I took the row and set it as the "Tag" of the Menu Item. Now we have context.


   1:  public virtual List<ContextMenuItemInfo> GetContextMenuContent(System.Windows.Browser.HtmlEventArgs e)
   2:  {
   3:      Point curr = new Point(e.OffsetX, e.OffsetY);
   4:      for (var i = 0; i < presenter.Children.Count; i++)
   5:      {
   6:          var row = (DataGridRow)presenter.Children[i];
   7:          if (ContextMenuInterceptor.IsPointWithinBounds(curr, row))
   8:          {
   9:              foreach (ContextMenuItemInfo m in ContextMenuList)
  10:              {
  11:                  m.Tag = row;
  12:              }
  13:              return ContextMenuList;
  14:          }
  15:      }
  16:      return null;
  17:  }