ContextMenu, DataContext oraz problemy z binding

Jakiś czas temu napotkałem na problem podczas próby przypisania własnego kontekstu kontrolce ContextMenu. Zacznijmy może od razu od przykładu XAML:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"        
        Title="Sample" Height="191" Width="337">    
    <Grid x:Name="LayoutRoot" Background="DarkBlue">
        <Grid.ContextMenu>
            <ContextMenu DataContext="{Binding SimpleViewModel}">
                <ContextMenu.Items>
                    <MenuItem Header="{Binding Text}"/>
                </ContextMenu.Items>
            </ContextMenu>
        </Grid.ContextMenu>            
    </Grid>
</Window>

Prosty widok  – zwykły Grid z kontekstowym menu. DataContext ustawiam na SimpleVIewModel a następnie binduje właściwość Text. Code-behind wygląda następująco:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        SimpleViewModel = new WpfApplication1.SimpleViewModel();
        DataContext = this;
        InitializeComponent();
    }
    public SimpleViewModel SimpleViewModel { get; private set; }
}
public class SimpleViewModel
{
    public string Text { get { return "Hello"; } }
}

Kod chyba nie wymaga komentarza. Po uruchomieniu aplikacji spodziewałem się, że w menu zostanie wyświetlony tekst “Hello”. Niestety ku mojemu zdziwieniu żaden tekst nie pojawił się. Po krótkim research’u  okazało się, że ContextMenu nie jest w WPF składową drzewa elementów (visual tree) i wszelkie bindingi (nawet te z ElementName) nie zadziałają. DataContext jest ustawiany na początku na kontekst rodzica i nie można go w podany sposób przeładować.

Istnieje na szczęście pewne, eleganckie obejście problemu. Otóż ContextMenu posiada właściwość PlacementTarget, które wskazuje na obiekt na którym znajduje się aktualnie menu (w tym przypadku Grid). Za pomocą PlacementTarget można zatem dostać się do DataContext rodzica a następnie do SimpleViewModel! Oto przykład:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"        
        Title="Sample" Height="191" Width="337">    
    <Grid x:Name="LayoutRoot" Background="DarkBlue">
        <Grid.ContextMenu>
            <ContextMenu DataContext="{Binding PlacementTarget.DataContext.SimpleViewModel,RelativeSource={RelativeSource Self}}">
                <ContextMenu.Items>
                    <MenuItem Header="{Binding Text}"/>
                </ContextMenu.Items>
            </ContextMenu>
        </Grid.ContextMenu>            
    </Grid>
</Window>

RelativeSelf ustawia jako kontekst obiekt kontrolki – uzyskujemy więc dostęp do wszelkich publicznych właściwości kontrolki (a więc również do PlacementTarget). Następnie poprzez DataContext dostajemy się do naszego SimpleViewModel. Po uruchomieniu aplikacji, przekonamy się, że teraz wiązanie danych zostało przeprowadzone poprawnie.

Leave a Reply

Your email address will not be published.