WPF – Caliburn.Micro – ContextMenu czyli jak skutecznie utrudnić sobie życie

Potrzebowałem pewien czas temu uzyskać taki w gruncie rzeczy prosty efekt. Po kliknięciu lewym przyciskiem myszki na butonie otwiera się menu z którego wybieramy interesującą opcję. Niby nic trudnego, ale ja przekombinowałem (nie pierwszy zresztą raz i nie ostatni). Na początek może jak wyglądałby kod takiego buttona:

<Button x:Name="MainButton" Content="Button">
    <Button.ContextMenu>
        <ContextMenu>
            <MenuItem x:Name="One" Header="Action 1"/>
            <MenuItem x:Name="Two" Header="Action 2"/>
            <MenuItem x:Name="Three" Header="Action 3"/>
        </ContextMenu>
    </Button.ContextMenu>
</Button>

Nic niezwykłego jak sądzę. Teraz tylko tak to zaimplementować aby otwierało się na lewy przycisk a nie na prawy. Najpierw może pokaże jak zrobiłem to w code-behind. Pierwsze co mi przyszło do głowy to MouseClick (MouseDown nie zadziała dla lewego przycisku myszy). Wystarczy więc podpiąć pod button zdarzenie MouseClick

private void MainButton_Click(object sender, RoutedEventArgs e)
{
    CtxMenu.IsOpen = true;
}

Ponieważ ostatnimi czasy cały kod, który piszę w WPF piszę z wykorzystaniem wzorca MVVM i Caliburn.Micro więc rozwiązanie z code-behind nie wchodziło w grę. Trzeba było to zrobić inaczej. Tylko, że im dalej w las tym ciemniej a droga coraz bardziej kręta. Najpierw może to co wydaje się oczywiste czyli binding:

<Button x:Name="MainButton" Content="Button">
    <Button.ContextMenu>
        <ContextMenu IsOpen="{Binding IsContextMenuOpen}">
            <MenuItem x:Name="One" Header="Action 1"/>
            <MenuItem x:Name="Two" Header="Action 2"/>
            <MenuItem x:Name="Three" Header="Action 3"/>
        </ContextMenu>
    </Button.ContextMenu>
</Button>

Kod jak kod poza tym, że ma jedną wadę. Nie działa. Przyczyna jest w gruncie rzeczy prosta Nie mamy otwartego menu, nie ma do czego bindować. Co nam więc pozostaje? Bardzo fajne rozwiązanie jest zaproponował Thomas Levesque. Tworzymy klasę BindingProxy

public class BindingProxy : Freezable
{
    protected override Freezable CreateInstanceCore()
    {
        return new BindingProxy();
    }

    public object Data
    {
        get { return (object)GetValue(DataProperty); }
        set { SetValue(DataProperty, value); }
    }

    public static readonly DependencyProperty DataProperty = DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}

A następnie w naszym View tworzymy coś takiego

<Button x:Name="MainButton" Content="Button">
    <Button.ContextMenu>
        <ContextMenu IsOpen="{Binding Data.IsContextMenuOpen, Source={StaticResource proxy}}">
            <MenuItem x:Name="One" Header="Action 1"/>
            <MenuItem x:Name="Two" Header="Action 2"/>
            <MenuItem x:Name="Three" Header="Action 3"/>
        </ContextMenu>
    </Button.ContextMenu>
</Button>

Oczywiście w metodzie MainButton ustawiamy property IsContextMenuOpen. I niby wszystko Ok tylko, że poszczególne przyciski w menu nie działają! Tzn. UnitTest przechodzi poprawnie, ale próba odpalenia tego z poziomu programu kończy się fiaskiem. Co jest??? Jak zwykle jest kilka rozwiązań. Ja na początek wybrałem to dłuższe. Otóż zmieniając sposób wywołania metod z bindowania po x:Name na Message.Attach zauważyłem, że nie następuje przekazanie żądania do ViewModelu wygląda to tak jakby poszukiwany był własny ViewModel. Chciałem tego uniknąć więc dołożyłem do elementu ContextMenu niewielki wpis dzięki, któremu metody były poszukiwane w bieżącym ViewModelu

<Button x:Name="MainButton" Content="Button">
    <Button.ContextMenu>
        <ContextMenu cal:Action.TargetWithoutContext="{Binding}" IsOpen="{Binding Data.IsContextMenuOpen, Source={StaticResource proxy}}">
            <MenuItem cal:Message.Attach="One" Header="Action 1"/>
            <MenuItem cal:Message.Attach="Two" Header="Action 2"/>
            <MenuItem cal:Message.Attach="Three" Header="Action 3"/>
        </ContextMenu>
    </Button.ContextMenu>
</Button>

przy czym musimy wywoływać metody z wykorzystaniem Message.Attach.
I to by było na tyle tylko, że … można to zrobić jeszcze prościej.

<Button x:Name="MainButton" Content="Button">
    <Button.ContextMenu>
        <ContextMenu IsOpen="{Binding Data.IsContextMenuOpen, Source={StaticResource proxy}}">
            <MenuItem cal:Message.Attach="ActionOne" Header="Action 1"/>
            <MenuItem cal:Message.Attach="ActionTwo" Header="Action 2"/>
            <MenuItem cal:Message.Attach="ActionThree" Header="Action 3"/>
        </ContextMenu>
    </Button.ContextMenu>
</Button>

Należy tylko zwrócić uwagę że ActionOne odpala metodę One a nie ActionOne!

Na zakończenie puenta. Cała ta robota poza tym, że poznałem lepiej framework (i miałem napisać o czym posta) poszła na marne. Ostatecznie zamiast ContextMenu wywoływany jest Popup.

_windowManager.ShowPopup(new PopupViewModel());

Cykl o MVVM

3 thoughts on “WPF – Caliburn.Micro – ContextMenu czyli jak skutecznie utrudnić sobie życie

  1. Pingback: dotnetomaniak.pl
  2. Wojtku,
    artykuł bardzo fajny, tylko jak dla mnie za mało wyjaśnień co do niektórych części kodu.
    Pozdrawiam
    Jarek

Comments are closed.