We use RadGridView fairly extensively in one of our WPF application, so when it comes to making common changes to this control it saves a lot of time and effort if we can minimise the number of places we need to make such modifications.
Telerik have examples that show how to incorporate the RadContextMenu inside RadGridView, but as at this time (version 2010.1.702.35) they show wire-up in code behind:
XAML
<Examples:GridViewExample
x:Class="Telerik.Windows.Examples.GridView.RowContextMenu.Example"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Examples="clr-namespace:Telerik.Windows.Examples"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation">
<Grid>
<telerik:RadGridView x:Name="RadGridView1"
ItemsSource="{Binding RandomProducts}"
CanUserFreezeColumns="False">
<telerik:RadContextMenu.ContextMenu>
<telerik:RadContextMenu Opened="RadContextMenu_Opened"
ItemClick="RadContextMenu_ItemClick">
<telerik:RadContextMenu.Items>
<telerik:RadMenuItem Header="Add" />
<telerik:RadMenuItem Header="Edit" />
<telerik:RadMenuItem Header="Delete" />
</telerik:RadContextMenu.Items>
</telerik:RadContextMenu>
</telerik:RadContextMenu.ContextMenu>
</telerik:RadGridView>
</Grid>
</Examples:GridViewExample>
C#
private void RadContextMenu_Opened(object sender, RoutedEventArgs e)
{
RadContextMenu menu = (RadContextMenu)sender;
GridViewRow row = menu.GetClickedElement<GridViewRow>();
if (row != null)
{
row.IsSelected = row.IsCurrent = true;
GridViewCell cell = menu.GetClickedElement<GridViewCell>();
if (cell != null)
{
cell.IsCurrent = true;
}
}
else
{
menu.IsOpen = false;
}
}
private void RadContextMenu_ItemClick(object sender, RadRoutedEventArgs e)
{
RadContextMenu menu = (RadContextMenu)sender;
RadMenuItem clickedItem = e.OriginalSource as RadMenuItem;
GridViewRow row = menu.GetClickedElement<GridViewRow>();
if (clickedItem != null && row != null)
{
string header = Convert.ToString(clickedItem.Header);
switch (header)
{
case "Add":
RadGridView1.BeginInsert();
break;
case "Edit":
RadGridView1.BeginEdit();
break;
case "Delete":
RadGridView1.Items.Remove(row.DataContext);
break;
default:
break;
}
}
}
The purpose of these two events:-
- When the right click occurs to display the menu, we need to select the item we are over.
- When the item is then clicked, the above code can determine the relevant row and perform some action.
Now given that we have a lot of GridViews wouldn’t it be great if we didn’t have to write this code for each instance? If we only have to modify the XAML to achieve the same result, we are halving the number of files we need to modify. Call me lazy, but that sounds great to me!
So here’s what we end up with in our XAML:
C#
<telerik:RadContextMenu.ContextMenu>
<telerik:RadContextMenu>
<helper:AttachedCommand.CommandHelper>
<helper:CommandHelper EventName="RadGridView.RadContextMenu.Opened" />
</helper:AttachedCommand.CommandHelper>
<telerik:RadContextMenu.Items>
<telerik:RadMenuItem Header="View"
Command="{Binding ViewCommand}"
CommandParameter="{Binding PlacementTarget.SelectedItem,
RelativeSource={RelativeSource AncestorType={x:Type telerik:RadContextMenu}}}" />
<telerik:RadMenuItem Header="Remove"
Command="{Binding RemoveCommand}"
CommandParameter="{Binding PlacementTarget.SelectedItem,
RelativeSource={RelativeSource AncestorType={x:Type telerik:RadContextMenu}}}" />
</telerik:RadContextMenu.Items>
</telerik:RadContextMenu>
</telerik:RadContextMenu.ContextMenu>
The AttachedCommand section deals with the Opened routed event, and is common for every paste we perform inside a GridView. The actions we need to perform make use of commands that are already defined and used via a custom toolbar:
C#
<pscc:ListEditToolbar DockPanel.Dock="Right"
ViewVisibility="Visible"
ViewCommand="{Binding ViewCommand}"
ViewCommandParameter="{Binding ElementName=_importDetails, Path=SelectedItem}"
RemoveVisibility="Visible"
RemoveCommand="{Binding RemoveCommand}"
RemoveCommandParameter="{Binding ElementName=_importDetails, Path=SelectedItem}"
/>
Command and CommandParameter
You can see from the existing use of the commands and parameters in the toolbar, that we get the SelectedItem of the GridView (called _importDetails in this case) and that gets passed when the command is executed.
Originally you might try doing exactly the same thing for each RadMenuItem. However, there are issues with the way we might normally try to find a control and walk the visual tree due to the way that a ContextMenu is created. As a result, we can make use of PlacementTarget on the ContextMenu: this gives us the RadGridView, and hence we can get the SelectedItem.
I found a good example and explanation of this on Patrick Long’s Blog.
However, for this to work we need to first handle the Opened event to ensure we select the item we are over.
Handling the Opened routed event
You can see from the XAML we defined that we now need to define 2 classes to deal with the Opened event, namely: AttachedCommand and CommandHelper. However, these classes are not restricted to just this one ‘event’, and in my application I am using them to deal with other scenarios as well (e.g. double click on a row to execute a command). I must give credit here to Hristo at Telerik for sending me down this path in 2009…I originally stumbled across the following article (which appears to have grown quite a bit since I last read it!), and then subsequent discussions with Hristo helped produce the following approach:
C# - Attached Command
public sealed class AttachedCommand
{
public static CommandHelper GetCommandHelper(DependencyObject obj)
{
return (CommandHelper)obj.GetValue(CommandHelperProperty);
}
public static void SetCommandHelper(DependencyObject obj,
CommandHelper value)
{
obj.SetValue(CommandHelperProperty, value);
}
public static readonly DependencyProperty CommandHelperProperty =
DependencyProperty.RegisterAttached("CommandHelper",
typeof(CommandHelper),
typeof(AttachedCommand),
new FrameworkPropertyMetadata(CommandHelperChanged));
private static void CommandHelperChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
FrameworkElement fe = d as FrameworkElement;
CommandHelper commandHelper = e.NewValue as CommandHelper;
if (fe != null
&& commandHelper != null
&& !string.IsNullOrEmpty(commandHelper.EventName))
{
switch (commandHelper.EventName)
{
// Note: Other Events/Commands we deal with go here
case "RadGridView.RadContextMenu.Opened":
RadContextMenuOpenedEventManager.
RemoveListener(fe, commandHelper);
RadContextMenuOpenedEventManager.
AddListener(fe, commandHelper);
break;
default:
throw new NotImplementedException(
"This routed event is not currently being handled");
}
}
}
}
This class basically provides us with a centralised location to wire-up to any events for controls that we want. You could implement that event wire-up directly in each case statement, but I found that doing this stopped garbage collection, so I ended up creating another class to implement a weak event pattern.
C# - Weak Event Manager
public sealed class RadContextMenuOpenedEventManager : WeakEventManager
{
private RadContextMenuOpenedEventManager()
{
}
public static void AddListener(UIElement source,
IWeakEventListener listener)
{
CurrentManager.ProtectedAddListener(source, listener);
}
public static void RemoveListener(UIElement source,
IWeakEventListener listener)
{
CurrentManager.ProtectedRemoveListener(source, listener);
}
protected override void StartListening(object source)
{
UIElement view = (UIElement)source;
view.AddHandler(RadContextMenu.OpenedEvent,
new RoutedEventHandler(this.OnRoutedEvent), true);
}
protected override void StopListening(object source)
{
UIElement view = (UIElement)source;
view.RemoveHandler(RadContextMenu.OpenedEvent,
new RoutedEventHandler(this.OnRoutedEvent));
}
private void OnRoutedEvent(object sender, RoutedEventArgs e)
{
base.DeliverEvent(sender, e);
}
private static RadContextMenuOpenedEventManager CurrentManager
{
get
{
Type managerType = typeof(RadContextMenuOpenedEventManager);
RadContextMenuOpenedEventManager currentManager =
(RadContextMenuOpenedEventManager)WeakEventManager
.GetCurrentManager(managerType);
if (currentManager == null)
{
currentManager = new RadContextMenuOpenedEventManager();
WeakEventManager.SetCurrentManager(managerType,
currentManager);
}
return currentManager;
}
}
}
Now all we need is the CommandHelper class that gets called by the WeakEventManager to perform the action.
C#
public sealed class CommandHelper : Freezable, IWeakEventListener
{
public static readonly DependencyProperty CommandProperty =
ButtonBase.CommandProperty.AddOwner(
typeof(CommandHelper));
public static readonly DependencyProperty CommandParameterProperty =
ButtonBase.CommandParameterProperty.AddOwner(
typeof(CommandHelper));
public static readonly DependencyProperty EventNameProperty =
DependencyProperty.Register("EventName", typeof(string),
typeof(CommandHelper));
public CommandHelper(){}
public string EventName
{
get { return (string)GetValue(EventNameProperty); }
set { SetValue(EventNameProperty, value); }
}
public object CommandParameter
{
get { return this.GetValue(CommandParameterProperty); }
set { this.SetValue(CommandParameterProperty, value); }
}
public ICommand Command
{
get { return (ICommand)this.GetValue(CommandProperty); }
set { this.SetValue(CommandProperty, value); }
}
protected override Freezable CreateInstanceCore()
{
// We are required to override this abstract method.
throw new NotImplementedException();
}
bool IWeakEventListener.ReceiveWeakEvent(Type managerType,
object sender, EventArgs e)
{
// Other manager type handling code goes here...
if (managerType == typeof(RadContextMenuOpenedEventManager))
{
RadContextMenu menu = (RadContextMenu)sender;
GridViewRow row = menu.GetClickedElement<GridViewRow>();
if (row != null)
{
row.IsSelected = row.IsCurrent = true;
GridViewCell cell = menu
.GetClickedElement<GridViewCell>();
if (cell != null)
{
cell.IsCurrent = true;
}
}
else
{
menu.IsOpen = false;
}
}
else
{
return false;
}
return true;
}
}
So you can see that the row selection code that was in the Telerik code behind example ends up in the ReceiveWeakEvent method.
With these 3 classes we can now modify all our RadGridView XAML definitions to contain a RadContextMenu, and make use of existing command definitions and avoid the need to write code behind for each instance.
Print | posted on Wednesday, 7 July 2010 12:03 PM