One of the great features of WPF is the concept of the visual tree. Every element we see on the screen is some where in the visual tree. It is pretty common in WPF to want to walk up this visual tree and grab some data for binding purposes. A simple RelativeSource Binding:
<Path Fill="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Button}}, Path=Foreground}" />
This will walk up the visual tree, until if finds the first Button, and set the Path.Fill property to the Foreground property of the Button. This works great, unless you want to find visual elements that are not in the same tree, as is the case with Popup or ContextMenu(which inherits from MenuBase). These elements will not be in the same visual tree as the rest of an application so a binding like this will not work:
<ContextMenu DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Button}}, Path=DataContext}">
Maybe a little unexpectedly, our data context will be null. However, we can get a reference to the visual element that opened the ContextMenu. The XAML for this looks like so:
<ContextMenu DataContext="{Binding RelativeSource={RelativeSource Mode=Self}, Path=PlacementTarget.DataContext}">
Be it good or bad, this only allows access to the DataContext of the PlacementTarget. But what if we have a little more complex situation? Let’s layout a hypothetical situation. Suppose we have a ListBox control that is data bound to a list of strings. For each string, we would like to have a ContextMenu that will allow us to delete the string from the list. Let’s start with this XAML:
<Window x:Class="MultiBindingList.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="300" Width="300"> <Grid> <ListBox ItemsSource="{Binding Path=Items}"> <ListBox.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding}"> <TextBlock.ContextMenu> <ContextMenu> <MenuItem Header="Delete" /> </ContextMenu> </TextBlock.ContextMenu> </TextBlock> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> </Window>
We have our ListBox, and we have it bound to a list of items, and we have put a ContextMenu in the ItemTemplate. So far so good. Now, let’s switch to our code behind:
public partial class Window1 : Window { public Window1() { InitializeComponent(); Items = new ObservableCollection<string>(new string[] { "Item 1", "Item 2", "Item 3" }); RemoveItemCommand = new DelegateCommand<string>(RemoveItemCommandExecutedHandler); DataContext = this; } public ObservableCollection<string> Items { get; set; } public ICommand RemoveItemCommand { get; set; } void RemoveItemCommandExecutedHandler(string item) { Items.Remove(item); } }
The only thing of note here is the DelegateCommand. It’s a class that implements ICommand and is frequently used in WPF to forward a command from a view to a view model. Now, in order to get the ContextMenu Command and CommandParameter property set correctly, we will have to pass in two object. The first will be whatever the ListBoxItem.DataContext property has been set to by WPF. The second will be the ICommand in the code behind, which we will get by walking the visual tree. But we can’t walk the visual tree you say? Enter the MultiValueBagConverter. First, let’s look at the code and then walk through it:
public class MultiValueBagConverter : IMultiValueConverter { #region IMultiValueConverter Members public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return new List<object>(values); } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } #endregion }
Here, we are using a IMultiValueConverter to transform all of our MultiBinding values to a List<object>. This should allow us to pass in any number of parameters, and use them by calling an index on our newly created list. Let’s update our XAML with the converter, setting the correct data contexts:
<Window x:Class="MultiBindingList.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Local="clr-namespace:MultiBindingList" Title="Window1" Height="300" Width="300"> <Window.Resources> <ResourceDictionary> <Local:MultiValueBagConverter x:Key="MultiValueBag" /> </ResourceDictionary> </Window.Resources> <Grid> <ListBox ItemsSource="{Binding Path=Items}"> <ListBox.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding Path=[0]}"> <TextBlock.DataContext> <MultiBinding Converter="{StaticResource MultiValueBag}"> <Binding /> <Binding Path="DataContext.RemoveItemCommand" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}" /> </MultiBinding> </TextBlock.DataContext> <TextBlock.ContextMenu> <ContextMenu DataContext="{Binding RelativeSource={RelativeSource Mode=Self}, Path=PlacementTarget.DataContext}"> <MenuItem Header="Delete" Command="{Binding Path=[1]}" CommandParameter="{Binding Path=[0]}" /> </ContextMenu> </TextBlock.ContextMenu></TextBlock> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> </Window>
First, we added a reference to our namespace, xmlns:Local. Second, we added a ResourceDictionary and added our converter to it, giving it a key of MultiValueBag. Hop down to the TextBlock, and we had to change our code around a bit. First, the Text property is now bound to index o of our list. Second, we set the DataContext of the TextBlock to the output of our converter, passing it in the ListBoxItem value, as well as the RemoveItemCommand we got from walking up to our Window and getting the DataContext from that. Now, our ContextMenu has it’s DataContext set to the PlacementTarget.DataContext, but now we have a list of all the items we need to execute the command. Right click to bring up the ContextMenu, push the delete menu item, an you will see the item’s disappear out of the list! Here is the source.
0 comments:
Post a Comment