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.

No comments:

Post a Comment