Archive

Archive for the ‘DataGrid’ Category

Moving WPF DataGrid Rows using Drag and Drop

March 24th, 2009

For my upcoming NetDrives tool (will be released shortly) I wanted to enable the user to reorder managed network shares using drag and drop using a preview of the dragged row:

image

 

As it turned out, it’s not too hard to implement, but it took my a while to find all pieces of the puzzle, so I compiled a short sample. You can find the sample link at the end of the article.

 

Drag Indicator Popup

I used a popup as an drag indicator, which I bound to the item that was currently dragged (DraggedItem dependency property):

<!-- the popup that is displayed if user moves rows -->
<Popup
  x:Name="popup1"
  IsHitTestVisible="False"
  Placement="RelativePoint"
  PlacementTarget="{Binding ElementName=me}"
  AllowsTransparency="True">
  <Border
    BorderBrush="{DynamicResource CellBorderBrush}"
    BorderThickness="2"
    Background="White"
    Opacity="0.75">
    <StackPanel
      Orientation="Horizontal"
      Margin="4,3,8,3">
      <Image
        Source="/Shared/Images/DragInsert.png"
        Width="16"
        Height="16" />
      <TextBlock
        Style="{DynamicResource DefaultLabel}"
        FontWeight="Bold"
        VerticalAlignment="Center"
        Text="{Binding ElementName=me, Path=DraggedItem.Name}"
        Margin="8,0,0,0" />
    </StackPanel>
  </Border>
</Popup>

 

Disabling Drag and Drop in Edit Mode

I didn’t want to enable drag and drop if the grid was in edit mode. Accordingly, I registered two event listeners on the grid:

<dg:DataGrid
  BeginningEdit="OnBeginEdit"
  CellEditEnding="OnEndEdit"
  .. />

 

The corresponding event listeners just set the IsEditing flag, which is evaluated when handling mouse events:

/// <summary>
/// State flag which indicates whether the grid is in edit
/// mode or not.
/// </summary>
public bool IsEditing { get; set; }

private void OnBeginEdit(object sender, DataGridBeginningEditEventArgs e)
{
  IsEditing = true;
  //in case we are in the middle of a drag/drop operation, cancel it...
  if (IsDragging) ResetDragDrop();
}

private void OnEndEdit(object sender, DataGridCellEditEndingEventArgs e)
{
  IsEditing = false;
}

 

Listening to Mouse Events

In order to display and move the popup with the mouse, I registered listeners for the following three mouse events:

  • PreviewMouseLeftButtonDown (on the datagrid)
  • MouseLeftButtonUp (directly on the layout root)
  • MouseMove (directly on the layout root)

 

Note: I started with listeners on the grid only, which caused some side effects. Apparently, the datagrid (current March release) not always fires the mouse events properly. This caused choppy animations when hovering over certain cells. Fortunately, this is not an issue with the MouseMove event of the layout root.

 

Starting Drag and Drop

DnD is started as soon as the user presses the left mouse button on the datagrid. I had to use the PreviewLeftMouseButton event in order to get the notification, and I needed to determine the clicked row based on the mouse position. I blogged about finding an element under the mouse a while ago here, but the UIHelpers class is part of the sample project here.

My mouse button event listener basically does the following:

  • Check if the mouse is being placed over a grid row.
  • Set the IsDragging flag to true.
  • Store the dragged item in the DraggedItem dependency property (used by the popup to display the name).

 

private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
  //exit if in edit mode
  if (IsEditing) return;

  //find the clicked row
  var row = UIHelpers.TryFindFromPoint<DataGridRow>((UIElement) sender,
                                                    e.GetPosition(shareGrid));
  if (row == null) return;

  //set flag that indicates we're capturing mouse movements
  IsDragging = true;
  DraggedItem = (IShareConfiguration) row.Item;
}

 

Moving the Popup

I registered a listener for the MouseMove event directly on the layout root (not on the datagrid). Basically, the event listener just moves the popup to the current mouse location along with a few minor tasks:

  • If the popup has not been opened yet, display it.
  • Set the grid to read-only.
  • Reposition the popup by setting the PlacementRectangle property.
  • Make sure the grid row under the mouse is being selected. Once again, this didn’t work reliably if I relied on the datagrid to do it by itself.

 

/// <summary>
/// Completes a drag/drop operation.
/// </summary>
private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
  if (!IsDragging || IsEditing)
  {
    return;
  }

  //get the target item
  ShareConfiguration targetItem = (ShareConfiguration) shareGrid.SelectedItem;

  if (targetItem == null || !ReferenceEquals(DraggedItem, targetItem))
  {
    //remove the source from the list
    ShareList.Remove(DraggedItem);

    //get target index
    var targetIndex = ShareList.IndexOf(targetItem);

    //move source at the target's location
    ShareList.Insert(targetIndex, DraggedItem);

    //select the dropped item
    shareGrid.SelectedItem = DraggedItem;
  }

  //reset
  ResetDragDrop();
}

 

Finishing Drag and Drop

Once the user releases the mouse button, I need to perform the actual drop operation. I already had the dragged item (DraggedItem property, was set when the operation started) so all I needed was the drop target. My target is the currently selected row.

/// <summary>
/// Completes a drag/drop operation.
/// </summary>
private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
  if (!IsDragging || IsEditing || shareGrid.SelectedItem == null)
  {
    return;
  }
  
  //get the target item
  IShareConfiguration targetItem = (IShareConfiguration)shareGrid.SelectedItem;

  if (!ReferenceEquals(DraggedItem, targetItem))
  {
    //the actual business logic that works on source and target
  }

  //reset
  ResetDragDrop();
}

 

You might also want to check whether the mouse is currently over the grid (or a grid row) or not – in that case just use the TryFindFromPoint method from the UIHelpers class.

 

Cleaning Up

The ResetDragDrop method just performs a cleanup of the code by closing the popup and adjusting a few properties:

/// <summary>
/// Closes the popup and resets the
/// grid to read-enabled mode.
/// </summary>
private void ResetDragDrop()
{
  IsDragging = false;
  popup1.IsOpen = false;
  shareGrid.IsReadOnly = false;      
}

 

Download Sample: http://blog.hardcodet.net/wp-content/uploads/2009/03/datagrid_dragdrop.zip

Author: Categories: DataGrid Tags: ,

Detecting Double Click Events on the WPF DataGrid

March 21st, 2009

Either I missed the obvious solution, or there is indeed no simple way to catch double click events on a given row of Microsoft’s WPF DataGrid.

This snippet here fires the event whenever the grid is clicked – it doesn’t matter whether the user double-clicks into an empty area (no rows) or a row:

<!-- fires even if the user does not click a given row -->
<dg:DataGrid
  dg:DataGridRow.MouseDoubleClick="OnDoubleClick" />

 

As a result, I reverted to a workaround by searching the Visual Tree of the event source for an instance of type DataGridRow. In order to find the ancestor item, I used a snippet I posted here a while ago. Here’s the full code:

 

XAML:

<!-- just register a listener on the grid -->
<dg:DataGrid
  MouseDoubleClick="OnDoubleClick" />

 

Code-Behind:

/// <summary>
/// Handles double-clicks on datagrid rows.
/// </summary>
private void OnDoubleClick(object sender, MouseButtonEventArgs e)
{
  //search the object hierarchy for a datagrid row
  DependencyObject source = (DependencyObject) e.OriginalSource;
  var row = UIHelpers.TryFindParent<DataGridRow>(source);

  //the user did not click on a row
  if (row == null) return;

  //[insert great code here...]

  e.Handled = true;
}

 

UIHelper class providing the TryFindParent method:

(Snippet updated: 2009.09.14)

/// <summary>
/// Finds a parent of a given item on the visual tree.
/// </summary>
/// <typeparam name="T">The type of the queried item.</typeparam>
/// <param name="child">A direct or indirect child of the
/// queried item.</param>
/// <returns>The first parent item that matches the submitted
/// type parameter. If not matching item can be found, a null
/// reference is being returned.</returns>
public static T TryFindParent<T>(this DependencyObject child)                               where T : DependencyObject
{
  //get parent item
  DependencyObject parentObject = GetParentObject(child);

  //we've reached the end of the tree
  if (parentObject == null) return null;

  //check if the parent matches the type we're looking for
  T parent = parentObject as T;
  if (parent != null)
  {
    return parent;
  }
  else
  {
    //use recursion to proceed with next level
    return TryFindParent<T>(parentObject);
  }
}

/// <summary>
/// This method is an alternative to WPF's
/// <see cref="VisualTreeHelper.GetParent"/> method, which also
/// supports content elements. Keep in mind that for content element,
/// this method falls back to the logical tree of the element!
/// </summary>
/// <param name="child">The item to be processed.</param>
/// <returns>The submitted item's parent, if available. Otherwise
/// null.</returns>
public static DependencyObject GetParentObject(this DependencyObject child)
{
  if (child == null) return null;
  
  //handle content elements separately
  ContentElement contentElement = child as ContentElement;
  if (contentElement != null)
  {
    DependencyObject parent = ContentOperations.GetParent(contentElement);
    if (parent != null) return parent;

    FrameworkContentElement fce = contentElement as FrameworkContentElement;
    return fce != null ? fce.Parent : null;
  }

  //also try searching for parent in framework elements (such as DockPanel, etc)
  FrameworkElement frameworkElement = child as FrameworkElement;
  if (frameworkElement != null)
  {
    DependencyObject parent = frameworkElement.Parent;
    if (parent != null) return parent;
  }

  //if it's not a ContentElement/FrameworkElement, rely on VisualTreeHelper
  return VisualTreeHelper.GetParent(child);
}

 

Enjoy 🙂

Author: Categories: DataGrid, Uncategorized Tags:

Microsoft WPF DataGrid vs. Commercial Solution: 1:0

December 30th, 2008

I’ve never been really happy with the commercial data grid I’ve been using so far – the whole API felt somewhat “winformish”, and required my to write a lot of XAML or even code for even the most basic tasks.

Today, I needed a simple grid on one of my current projects, and immediately got annoyed by the same issues that bug me every time. With the difference that this time, I had a new alternative to look at – Microsoft’s Data Grid that went V1 this October.

Well: This is an amazing control. It’s not amazingly powerful, it doesn’t have amazing animations, amazing views or anything like that. There is just one thing: It gets the job done.

Here’s my top 3 in comparison to my commercial product:

1: Data Binding

Finally I have data binding the way I always thought it should be. No more dealing with the internal data representation of the grid, and 2-way-data binding to selected items (not records or rows) out of the box. I didn’t even have to look up the API – it’s just as I expected it to be. Loving it:

 

<dg:DataGrid
  x:Name="platforms"
  ItemsSource="{Binding Path=Track.Platforms, ElementName=me}"
  SelectedItem="{Binding Path=ActivePlatform, ElementName=me}">

<!-- column definitions -->

</dg:DataGrid>

 

2: Liquid (Star-Sized) Columns

To my great surprise, I can easily star-size columns to take the full available horizontal space of the grid. This is a feature I need as good as every time I use a grid. With my commercial product, I was forced to write a whole layout template in order to get there. Not anymore:

 

<dg:DataGrid.Columns>
  <!-- fixed size column -->
  <dg:DataGridTextColumn
    Header="Name"
    Width="60" 
    Binding="{Binding Path=ItemName}" />
  <!-- takes 2/3 of the remaining space -->
  <dg:DataGridTextColumn
    Header="Description"
    Width="2*"
    Binding="{Binding Path=Description}" />
  <!-- takes 1/3 of the remaining space -->
  <dg:DataGridTextColumn
    Header="Position"
    Width="*"
    Binding="{Binding Path=StartPosition}" />
</dg:DataGrid.Columns>

 

3: Styling

I really do like some of the carefully crafted themes of my commercial grid. But on the other hand, customizing it was a major pain, so styling was one of my main concerns. However, the MS grid is amazingly flexible and easy to use for a v1.0 solution.

In the end, I’m happier with my custom-styled result than the predefined theme of my commercial grid – simply because the custom style blends in perfectly with the rest of the UI:

styledgrid

checklist

 

Conclusion

I’m pretty sure that the commercial – and definitely more powerful – solutions have their rightful place on the market, but Microsoft’s grid really fills a gap for me here – especially because of the API that just keeps things simple.

I can’t help but think that the vendors that were the first ones on the market may be last in the long run. At least the API of the grid I used so far just doesn’t cut it for me. We all had to get (are still getting) acquainted to “thinking in WPF” and some of the “mature” solutions just give me the impression that their basic concepts have “Windows Forms” written all over them. And with regards to compatibility, it might get pretty hard to get rid of that. That is, however, just my 0.02$.

Go get it @ CodePlex: http://www.codeplex.com/wpf

Author: Categories: DataGrid, Open Source, WPF, WPF Controls Tags: ,