Tuesday, March 30, 2010

WPF DataGrid ScrollIntoView

Technorati Tags: ,,,

In my previous post I talked about using Attached Behaviors to scroll selected item into view. It turns out there is a bug in WPF DataGrid and ScrollIntoView could sometimes throw NullReferenceException when VirtualizingStackPanel.IsVirtualizing="True" .

To avoid this exception there was a solution suggested on this forum http://wpf.codeplex.com/Thread/View.aspx?ThreadId=39458 which basically executes ScrollIntoView on a thread with a very low priority.

Here is my previous solution with suggested work around.

public class DataGridBehavior
{
#region AutoScrollIntoView

public static bool GetAutoScrollIntoView(DataGrid dataGrid)
{
return (bool)dataGrid.GetValue(AutoScrollIntoViewProperty);
}

public static void SetAutoScrollIntoView(
DataGrid dataGrid, bool value)
{
dataGrid.SetValue(AutoScrollIntoViewProperty, value);
}

public static readonly DependencyProperty AutoScrollIntoViewProperty =
DependencyProperty.RegisterAttached(
"AutoScrollIntoView",
typeof(bool),
typeof(DataGridBehavior),
new UIPropertyMetadata(false, OnAutoScrollIntoViewWhenSelectionChanged));

static void OnAutoScrollIntoViewWhenSelectionChanged(
DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
DataGrid dataGrid = depObj as DataGrid;
if (dataGrid == null)
return;

if (!(e.NewValue is bool))
return;

if ((bool)e.NewValue)
dataGrid.SelectionChanged += OnDataGridSelectionChanged;
else
dataGrid.SelectionChanged -= OnDataGridSelectionChanged;
}

static void OnDataGridSelectionChanged(object sender, RoutedEventArgs e)
{
// Only react to the SelectionChanged event raised by the DataGrid
// Ignore all ancestors.
if (!Object.ReferenceEquals(sender, e.OriginalSource))
return;

DataGrid dataGrid = e.OriginalSource as DataGrid;
if (dataGrid != null && dataGrid.SelectedItem != null)
{
// this is a workaround to fix the layout issue.
// otherwise ScrollIntoView should work directly.
dataGrid.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new DispatcherOperationCallback(ScrollItemIntoView),dataGrid);
}
}

static object ScrollItemIntoView(object sender)
{
DataGrid dataGrid = sender as DataGrid;
if (dataGrid != null && dataGrid.SelectedItem != null)
{
dataGrid.ScrollIntoView(dataGrid.SelectedItem);
}
return null;
}

#endregion // AutoScrollIntoView





Happy Coding!

Wednesday, March 24, 2010

Attached Behaviors

It is kind of late in the game, but I thought I would cover how to bring WPF DataGrid selected item into view using Attached Behaviors. There is a nice article by Josh Smith which covers Attached Behaviors in more details. I will just cover a specific case with DataGrid.

When you are using MVVM sometimes there is a business need to change selected item from ViewModel and when it happens occasionally users will need to manually scroll to that item. This could be very confusing to the user. The desired behavior would be to scroll selected item into the view automatically. 

There are multiple ways to solve this problem:

- Create an event handler for DataGrid.SelectionChanged event. If you however have multiple DataGrids in your project your code behind file will be polluted with these handlers. This is exactly the case why we are using MVVM to avoid code in code behind files and have a clear separation of concerns.

- Second approach will require extending DataGrid class and adding desired behavior. This is an overkill, since now everybody on the project will need to remember to use custom DataGrid. And if there are many of them it requires changes.

- Third approach using Attached Behaviors is very lightweight, “XAML friendly” and preserves MVVM separation of concerns.  All you need is to create a separate class which can be used sparingly as developers see fit, it can sit in your project and be handy when need arises.

Below is an example of such class I used for DataGrid.

namespace MyProject.AttachedBehaviors
{
public class DataGridBehavior
{
#region AutoScrollIntoView

public static bool GetAutoScrollIntoView(DataGrid dataGrid)
{
return (bool)dataGrid.GetValue(AutoScrollIntoViewProperty);
}

public static void SetAutoScrollIntoView(
DataGrid dataGrid, bool value)
{
dataGrid.SetValue(AutoScrollIntoViewProperty, value);
}

public static readonly DependencyProperty AutoScrollIntoViewProperty =
DependencyProperty.RegisterAttached(
"AutoScrollIntoView",
typeof(bool),
typeof(DataGridBehavior),
new UIPropertyMetadata(false, OnAutoScrollIntoViewWhenSelectionChanged));

static void OnAutoScrollIntoViewWhenSelectionChanged(
DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
DataGrid dataGrid = depObj as DataGrid;
if (dataGrid == null)
return;

if (!(e.NewValue is bool))
return;

if ((bool)e.NewValue)
dataGrid.SelectionChanged += OnDataGridSelectionChanged;
else
dataGrid.SelectionChanged -= OnDataGridSelectionChanged;
}

static void OnDataGridSelectionChanged(object sender, RoutedEventArgs e)
{
// Only react to the SelectionChanged event raised by the DataGrid
// Ignore all ancestors.
if (!Object.ReferenceEquals(sender, e.OriginalSource))
return;

DataGrid dataGrid = e.OriginalSource as DataGrid;
if (dataGrid != null && dataGrid.SelectedItem != null)
dataGrid.ScrollIntoView(dataGrid.SelectedItem);
}

#endregion // AutoScrollIntoView

}
}

Now is XAML you will need to reference the above namespace:

xmlns:localBehaviors="clr-namespace:MyProject.AttachedBehaviors"

And use it in your DataGrid in the following manner whenever you like to see such behavior.

<wpfToolkit:DataGrid 
EnableColumnVirtualization="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"
Grid.Row="0"
DataContext="{Binding}"
ItemsSource="{Binding Path=Entities}"
SelectedItem="{Binding
Path=EntityNavigation.CurrentEntity,
UpdateSourceTrigger=PropertyChanged,
Mode=TwoWay,
Converter={StaticResource ChildToParentEntityViewModelConverter}}"

localBehaviors:DataGridBehavior.AutoScrollIntoView="True"

...
/>

This (localBehaviors:DataGridBehavior.AutoScrollIntoView="True") last line in the XAML markup above will do it.

This was another ISolvable<TProblem>.

Happy Coding!

Tuesday, March 23, 2010

Avoid memory leaks in .NET

There is enough said about memory leaks in .NET when using events. BING or Google for "weak event". The problem is that when you have an observer for some object's event myObj.MyEvent when observer is no longer needed myObj.MyEvent still keeps reference to observer's event handling delegate. Thus preventing observer from being garbage collected. The solution is to always unsubscribe observer from any events it might listen to before disposing it. Well, this is easier said than done.

One way to deal with this issue is to create weak event references. There are many patterns and frameworks which support weak events such as EventAggregator in Prism or in WPF MVVM app template there is WeakEventReference.

The other way is to keep reference to event handler and always unsubscribe before disposing. This could be tricky.

The third way is very simple but covers only a subset of all possible event scenarios. This is what I will show in here. Suppose you have a handler which is no longer needed after event is fired. Then you can use lambda expression or anonymous delegate and keep reference to it only until it is in the scope. Here is an example. In this example I actually have two handlers which are not needed after either one of them is executed.  

   1: // declare a reference to event handler
   2: EventHandler myEvent1Result = null;
   3: EventHandler myEvent2Result = null;
   4:  
   5: // when MyEvent1 fires we handle it like so 
   6: myEvent1Result = (s, ea) =>
   7:     {
   8:         // unsubscribe from events immedeately
   9:         (s as MyObject).MyEvent1Event -= myEvent1Result;
  10:         (s as MyObject).MyEvent2Event -= myEvent2Result;
  11:  
  12:         // do some other processing
  13:         //....
  14:     };
  15:  
  16: // when MyEvent2 fires we handle it like so 
  17: myEvent2Result = (s, ea) =>
  18:    {
  19:        // unsubscribe from events immedeately
  20:        (s as MyObject).MyEvent1Event -= myEvent1Result;
  21:        (s as MyObject).MyEvent2Event -= myEvent2Result;
  22:  
  23:        // do some other processing
  24:        //....
  25:    };
  26:  
  27: // once we created event handlers now we can subscribe them.
  28: myObject.MyEvent1Event += myEvent1Result;
  29: myObject.MyEvent2Event += myEvent2Result; 

Happy coding!