Archive

Archive for March, 2009

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:

Format WPF Bindings With Resource File Lookups

March 21st, 2009

This post explains a pattern that combines Data Binding, the new StringFormat property and resource lookups through the application of a custom MarkupExtension.

 

StringFormat

Microsoft introduced the StringFormat property with SP1 for .NET 3.5. StringFormat often eliminates the need for a custom converter if you just need to format a bound value.
Here’s a very simple sample that displays hello xxx on the screen (where “xxx” is the value entered in a TextBox control:

 

<TextBox
    x:Name="name"
    Text="world" />

<TextBlock
    Text="{Binding ElementName=name,
                   Path=Text,
                   StringFormat=hello {0}}" />

 

Resource Files

I often use resource files to store strings – not only for localization tasks but also in order to maintain strings that are intended for users at a central place. My resource strings often contain placeholders which are populated at runtime using String.Format:

 

image

 

//use resource file to create message
var msg = Resources.Ask_Whether_To_Connect_To_New_Share;
msg = String.Format(msg, configuration.Name);

//display dialog
MessageBoxResult doConnect = Dialogs.ShowYesNo(msg);

 

 

Bringing Bindings and Resource Files Together

In the sample above, I accessed the resource file programmatically. In XAML however, I prefer a declarative approach. In order to get there, I implemented a simple MarkupExtension that allows me to do just that:

 

<!-- Display a formatted text --> <TextBlock x:Name="Title" Text="{Binding Path=Name, StringFormat={ext:Resource Ask_Whether_To_Connect_To_New_Share}}"

/>

 

The above snippet results in a formatted text being displayed:

image

 

Resource MarkupExtension

Here’s the markup extension. As you can see, it’s fairly trivial. Do note that the reference to the application’s Resources file is hardcoded – you will have to set a using statement or adjust that line in order to use your custom resource file.

 

/// <summary>
/// A markup extension that provides simple access to a given
/// entry in the application's <see cref="Resources"/> file.
/// </summary>
public class Resource : MarkupExtension
{
  /// <summary>
  /// The resource key to be used for the lookup.
  /// </summary>
  public string ResourceKey { get; set; }


  /// <summary>
  /// Inits the <see cref="Resource"/> markup extension
  /// with the key to be assigned.
  /// </summary>
  /// <param name="resourceKey">The resource key to be assigned.</param>
  public Resource(string resourceKey)
  {
    ResourceKey = resourceKey;
  }


  /// <summary>
  /// Performs a lookup for the defined <see cref="ResourceKey"/>.
  /// </summary>
  /// <returns>
  /// The value of the resource that is specified by the
  /// <see cref="ResourceKey"/> property. If the property is not
  /// set, a null reference is returned.
  /// </returns>
  public override object ProvideValue(IServiceProvider serviceProvider)
  {
    if (String.IsNullOrEmpty(ResourceKey)) return null;
    return Resources.ResourceManager.GetObject(ResourceKey);
  }
}

 

Enjoy 🙂

Author: Categories: WPF Tags: ,