RadMenu and RadMenuItem Caliburn.Micro

2019-07-18 14:20发布

问题:

I load programatically a radMenu with no problems using Caliburn.Micro, the Xaml looks like this:

<telerik:RadMenu ItemsSource="{Binding .MenuItems}"
                 VerticalAlignment="Top"
                 cal:Action.TargetWithoutContext="{Binding RelativeSource={RelativeSource Self}}"
                 cal:Message.Attach="[Event ItemClick] = [Action MenuItemClick($this)]">
  <telerik:RadMenu.ItemContainerStyle>
    <Style TargetType="telerik:RadMenuItem">
      <Setter Property="Tag"
              Value="{Binding .Tag}" />
      <Setter Property="Header"
              Value="{Binding .Text}" />
      <Setter Property="Icon"
              Value="{Binding .Image}" />
      <Setter Property="ItemsSource"
              Value="{Binding .SubItems}" />
      <Setter Property="Command"
              Value="{Binding .SubItems}" />
    </Style>
  </telerik:RadMenu.ItemContainerStyle>
</telerik:RadMenu>

On my ViewModel I have the corresponding MenuItems property which I fill from a database. The code looks like this:

Property MenuItems As New ObservableCollection(Of MenuItem)

Public Sub MenuItemClick(item As MenuItem)
    MessageBox.Show(item.Tag)
End Sub

The problem is the wiring of the ItemClick Event, I need to receive the radMenuItem object, I mean, I need to know which MenuItem was clicked.

I'v tried various combinations on the Action.TargetWithoutContext property, so far, I've only get the MenuItems collection.

Thanks in advance

回答1:

The clicked item will be in the RadRoutedEventArgs on the event callback in the property OriginalSource. e.g.

void RadMenu_ItemClick(object sender, Telerik.Windows.RadRoutedEventArgs e)
{
    var menuItem = e.OriginalSource as RadMenuItem;
}

Since RadRoutedEventArgs subclasses System.Windows.EventArgs you should be able to extract OriginalSource from either.

There are a couple of approaches (one of which I think is the better way to do it)

Approach 1 (works but too much work in VM for my liking):

Just pass the event args to the handler method on your VM, then you can extract the selected item in the VM code

cal:Message.Attach="[Event ItemClick] = [Action MenuItemClick($eventargs)]"

public class ViewModel 
{
    public void MenuItemClick(System.Windows.EventArgs e) 
    {
        var menuItem = e.OriginalSource;
        // menuItem should be RadMenuItem, you can use FrameworkElement base type to get DataContext
        var fe = menuItem as FrameworkElement;
        var data = fe.DataContext; // (obviously do your null checks etc!)
    }
}

However, this has the problem that it leaves the VM the concern of figuring out how to get the selected item from the eventargs. I don't like this approach much as it's prone to breaking if you change the control you are using etc.

Approach 2:

I assume that your MenuItem class is a custom class? You don't really want to put a dependency on 3rd party types in your VM code (in case you change to another control provider such as Infragistics or what have you) so you should be passing the actual bound object back to your viewmodel on click. If not this approach will still work (but you'll end up with a RadMenuItem reference in your VM)

You can extract the originalsource or actual bound item by customising Caliburn.Micro's MessageBinder.SpecialValues collection, and then pass the selected item directly to the VM. (You can put this code into your Bootstrapper somewhere)

Here is the approach to get the data item bound to the selected menu item:

MessageBinder.SpecialValues.Add("$selecteditem", (context) =>
{
    if (context.EventArgs is EventArgs)
    {          
        var e = context.EventArgs as EventArgs;

        // If the control is a FrameworkElement it will have a DataContext which contains the bound item
        var fe = e.OriginalSource as FrameworkElement;

        if (fe != null)
            return fe.DataContext;
    }

    return null;
});

If you want the actual RadMenuItem itself you can just change the above implementation to:

MessageBinder.SpecialValues.Add("$selecteditem", (context) =>
{
    if (context.EventArgs is EventArgs)
    {          
        var e = context.EventArgs as EventArgs;
        return e.OriginalSource;
    }

    return null;
});

And to use in XAML:

cal:Message.Attach="[Event ItemClick] = [Action MenuItemClick($selecteditem)]"

The good thing about this approach is that the ViewModel simply receives the bound item and it doesn't need to know about how to extract the value:

public class ViewModel 
{
    public void MenuItemClick(TheActualTypeThatWasBound item) 
    {
        // Do stuff with item
    }
}

Unless of course you are passing back the actual menu item:

public class ViewModel 
{
    public void MenuItemClick(RadMenuItem item) 
    {
        // Do stuff with item
        var boundData = item.DataContext;
    }
}

But I strongly suggest not to do this (I've got a pretty decent sized project using Rad controls and I've never needed a reference to any Rad control from within the VM)

Sorry I can't really VB it as I don't use VB but you can convert on this site:

http://www.developerfusion.com/tools/convert/vb-to-csharp/

Disclaimer:

$selecteditem is probably a bad name for it - maybe $originalsourcedatacontext but that's a bit of a mouthful :)