Use ICommand and IResult

Dec 10, 2010 at 3:25 PM

Hello all,

I use the latest version of Office Ribbon control, which supports MVVM.

For exemple, to bing the properties of a RibbonButton, there is a ButtonData, with these properties:

[NotifyPropertyChanged]
public class ControlData
{
    public string Label { get; set; }

    public Uri LargeImage { get; set; }

    public Uri SmallImage { get; set; }

    public string ToolTipTitle { get; set; }

    public string ToolTipDescription { get; set; }

    public Uri ToolTipImage { get; set; }

    public string ToolTipFooterTitle { get; set; }

    public string ToolTipFooterDescription { get; set; }

    public Uri ToolTipFooterImage { get; set; }

    public ICommand Command { get; set; }

    public string KeyTip { get; set; }
}

The binding is done like this:

<!-- RibbonControl -->
<Style x:Key="RibbonControlStyle">
    <Setter Property="r:RibbonControlService.Label" Value="{Binding Label}" />
    <Setter Property="r:RibbonControlService.LargeImageSource" Value="{Binding LargeImage}" />
    <Setter Property="r:RibbonControlService.SmallImageSource" Value="{Binding SmallImage}" />
    <Setter Property="r:RibbonControlService.ToolTipTitle" Value="{Binding ToolTipTitle}" />
    <Setter Property="r:RibbonControlService.ToolTipDescription" Value="{Binding ToolTipDescription}" />
    <Setter Property="r:RibbonControlService.ToolTipImageSource" Value="{Binding ToolTipImage}" />
</Style>
<!-- RibbonButton -->
<DataTemplate DataType="{x:Type data:ButtonData}">
    <r:RibbonButton />
</DataTemplate>
<Style TargetType="{x:Type r:RibbonButton}" BasedOn="{StaticResource RibbonControlStyle}">
    <Setter Property="Command" Value="{Binding Command}" />
</Style>

As you can see, it uses the ICommand interface for the action to execute when the user clicks on the button.

I use it like this:

[NotifyOfPropertyChange]
public class ShellViewModel : Conductor<IScreen>, IShellViewModel
{
    [Inject]
    public ShellViewModel()
    {
        var tabData = new TabData("Management");
        var groupData = new GroupData("Teams");
        var display = new ButtonData()
                        {
                            Label = "Display",
                            Command = //How to execute DisplayTeamsManagementView() ???
                        };

        groupData.ControlDataCollection.Add(display);
        tabData.GroupDataCollection.Add(groupData);
        TabDataCollection.Add(tabData);
    }


    public IEnumerable<IResult> DisplayTeamsManagementView()
    {
        yield return Show.Child<TeamsManagementViewModel>().In(this);
    }

    public ObservableCollection<TabData> TabDataCollection { get; private set; }
    public ObservableCollection<ContextualTabGroupData> ContextualTabGroupDataCollection { get; private set; }
    public MenuButtonData ApplicationMenuData { get; private set; }
}

I'd like to use the IResult interface and the coroutines, on top of the ICommand interface.

How do you think I should do, if it is possible?

I know it is possible to use "Message.Attach" within the XAML file, as I already dit it successfully, but I need to construct the Ribbon control from code.

Thanks in advance

Coordinator
Dec 10, 2010 at 3:59 PM

If you need to construct it in code, you can use Message.SetAttach and pass it the same string you would in xaml. Also, you should be able to do this in a style as well. Perhaps something like:

<Style TargetType="{x:Type r:RibbonButton}" BasedOn="{StaticResource RibbonControlStyle}">
    <Setter Property="cal:Message.Attach" Value="[Event Click] = [Action DoSomething]" />
</Style>

Dec 11, 2010 at 5:59 PM

Thanks for your answer.

As I construct the ribbon with code in the view-model, how do I get the required DependencyObject parameter of Message.SetAttach ?

Coordinator
Dec 11, 2010 at 10:49 PM

The DO would be the RibbonButton to which you are attaching the message.

Dec 12, 2010 at 8:44 AM

OK, but I don't have access to the RibbonButton, from the VM where I build up the menu.

I have several POCO classes, each of them used to set the properties of different parts of the ribbon (Tab, Groups, Buttons...).

For example, for Tabs and Groups, I have these styles:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:r="http://schemas.microsoft.com/winfx/2006/xaml/presentation/ribbon" xmlns:data="clr-namespace:Emidee.Wpf.Ribbon" xmlns:cal="clr-namespace:Caliburn.Micro;assembly=Caliburn.Micro">
    <BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
    
    <!-- RibbonTab -->
    <Style TargetType="{x:Type r:RibbonTab}">
        <Setter Property="ContextualTabGroupHeader" Value="{Binding ContextualTabGroupHeader}" />
        <Setter Property="Header" Value="{Binding Header}" />
        <Setter Property="ItemsSource" Value="{Binding GroupDataCollection}" />
    </Style>
    <!-- RibbonContextualTabGroup -->
    <Style TargetType="{x:Type r:RibbonContextualTabGroup}">
        <Setter Property="Header" Value="{Binding Header}" />
        <Setter Property="Visibility" Value="{Binding IsVisible,Converter={StaticResource BoolToVisibilityConverter}}" />
    </Style>
    <!-- RibbonControl -->
    <Style x:Key="RibbonControlStyle">
        <Setter Property="r:RibbonControlService.Label" Value="{Binding Label}" />
        <Setter Property="r:RibbonControlService.LargeImageSource" Value="{Binding LargeImage}" />
        <Setter Property="r:RibbonControlService.SmallImageSource" Value="{Binding SmallImage}" />
        <Setter Property="r:RibbonControlService.ToolTipTitle" Value="{Binding ToolTipTitle}" />
        <Setter Property="r:RibbonControlService.ToolTipDescription" Value="{Binding ToolTipDescription}" />
        <Setter Property="r:RibbonControlService.ToolTipImageSource" Value="{Binding ToolTipImage}" />
    </Style>
    <!-- RibbonGroup -->
    <Style TargetType="{x:Type r:RibbonGroup}" BasedOn="{StaticResource RibbonControlStyle}">
        <Setter Property="QuickAccessToolBarId" Value="{Binding Label}" />
        <Setter Property="Header" Value="{Binding Label}" />
        <Setter Property="ItemsSource" Value="{Binding ControlDataCollection}" />
        <!-- <Setter Property="GroupSizeDefinitions" Value="{Binding GroupSizeDefinitions, Converter={StaticResource groupSizeDefinitionsConverter}}" /> -->
    </Style>
</ResourceDictionary>

and these POCO:

[NotifyPropertyChanged]
public class TabData
{
    private ObservableCollection<GroupData> groupDataCollection = new ObservableCollection<GroupData>();

    public TabData()
        : this(null)
    {
    }

    public TabData(string header)
    {
        Header = header;
    }

    public string Header { get; set; }

    public string ContextualTabGroupHeader { get; set; }

    public bool IsSelected { get; set; }

    public ObservableCollection<GroupData> GroupDataCollection { get { return groupDataCollection; } set { groupDataCollection = value; } }
}

public class GroupData : ControlData
{
    public GroupData(string header)
    {
        Label = header;

        ControlDataCollection = new ObservableCollection<ControlData>();
        GroupSizeDefinitions = new ObservableCollection<GroupSizeDefinition>();
    }

    [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
    public ObservableCollection<ControlData> ControlDataCollection { get; private set; }
    public ObservableCollection<GroupSizeDefinition> GroupSizeDefinitions { get; private set; }
}

The RibbonControl is defined like this:

<r:Ribbon x:Name="Ribbon" ItemsSource="{Binding TabDataCollection}"
    ContextualTabGroupsSource="{Binding ContextualTabGroupDataCollection}"/>

And I create the TabDataCollection like this, in my ViewModel:

var tabData = new TabData("Management");
var groupData = new GroupData("Teams");
var display = new ButtonData()
                {
                    Label = "Display",
                    Command = DisplayTeamsManagementView()
                };
groupData.ControlDataCollection.Add(display);
tabData.GroupDataCollection.Add(groupData);
TabDataCollection.Add(tabData);

 

So, as you can see, I don't have access to the view and it's controls.

But as I can modify the classes used for the bindings, I can change the signature of the Command property of the ButtonData class, to return IEnumerable<IResult>, instead of ICommand. So how would I bind this routine to the click event of the RibbonButton? Should I create a new trigger?

I hope I have been clear enough :)

Thanks in advance

Mike

Dec 12, 2010 at 9:52 AM

I think you might just add a string property to ButtonData model, containing the message you would like to attach to the button:

public class ButtonData
{
	... 
    public string Message { get; set; }
    ...
}


public ShellViewModel()
{
	...
	var display = new ButtonData()
	{
		Label = "Display",
		Message = "[Event Click] = [Action DisplayTeamsManagementView]"
	};
	...
}

 

<Style TargetType="{x:Type r:RibbonButton}" BasedOn="{StaticResource RibbonControlStyle}">
    <Setter Property="cal:Message.Attach" Value="{Binding Message}" />
</Style>

Dec 13, 2010 at 12:04 PM

in my app I'm using commanding and coroutines too ... because the core of MessageAction is just calling Coroutine.Execute(...), I implemented both concepts via custom command class derived from Prism's DelegateCommand

 

public class DelegateResultCommand<T> : DelegateCommand<T> where T : class
{
public DelegateResultCommand(
Func<T, IEnumerable<IResult>> coroutine,
Func<T, bool> canExecuteMethod
) : base(delegate(T param) { Coroutine.Execute(coroutine(param)); }, canExecuteMethod)
{

  }
}

Dec 13, 2010 at 1:54 PM

Thanks! This works very well.

Anyway, I have a subsidiary question :)

I create the default tabs in my IShellViewModel. When I activate an item within my ShellView, this item may need to add tabs with buttons to the ribbon.

If I set the Message property of the buttons, so that it executes an action on the ActiveItem, I have the following unhandled exception, thrown in ActionMessage.Invoke:

Unhandled exception : No target found for method Test.

Here is the code I tested:

public class ShellViewModel
{
    public IEnumerable<IResult> DisplayTeamsManagementView()
    {
        ActivateManagementContextualTabs(teams: true);

        var displayTeamsManagementView = Show.Child<TeamsManagementViewModel>().In(this);
        yield return displayTeamsManagementView;

        TabDataCollection.Add(displayTeamsManagementView.Child.TabData);
    }
}

public class TeamsManagementViewModel
{
    public TabData TabData
    {
        get
        {
            var tabData = new TabData("Teams")
                            {
                                ContextualTabGroupHeader = "Management"
                            };

            var groupData = new GroupData("Actions");
            tabData.GroupDataCollection.Add(groupData);
            groupData.ControlDataCollection.Add(new ButtonData()
            {
                Label = "Test",
                Message = "[Event Click] = [Action Test]"
            });

            return tabData;
        }
    }

    public IEnumerable<IResult> Test()
    {
        yield return Show.MessageBox("Foo");
    }

    public bool CanTest
    {
        get { return true; }
    }
}

Before I used the RibbonControl with MVVM, I had to specify the TargetWithoutContext property. I was hoping I could simplify things by specifying the actions directly from the concerned view-model.

Do you have any idea on how I could achieve this?

Thanks in advance

Dec 14, 2010 at 9:46 AM

I've tried to set the TargetWithoutContext property using the style, and with a new property to ButtonData, but this doesn't work either:

 

<Style TargetType="{x:Type r:RibbonButton}" BasedOn="{StaticResource RibbonControlStyle}">
    <Setter Property="cal:Action.TargetWithoutContext" Value="{Binding Target}" />
    <Setter Property="cal:Message.Attach" Value="{Binding Message}" />
</Style>

public class ButtonData : ControlData
{
    public string Message { get; set; }
    public object Target { get; set; }
}

public class TeamsManagementViewModel
{
    public TabData TabData
    {
        get
        {
            var tabData = new TabData("Teams")
                            {
                                ContextualTabGroupHeader = "Management"
                            };

            var groupData = new GroupData("Actions");
            tabData.GroupDataCollection.Add(groupData);
            buttonData = new ButtonData()
                            {
                                Label = "Test",
                                Message = "[Event Click] = [Action Test]",
                                Target = this
                            };


            groupData.ControlDataCollection.Add(buttonData);

            return tabData;
        }
    }
}

 

What I don't understand, is that the TargetWithoutContext property doesn't seem to be set. I put some breakpoints in the code, and here is how things are done:

I pass in Action.OnTargetWithoutContextChanged, which calls Action.SetTargetCore.

When I click on the button, ActionMessage.SetMethodBinding is called. This method calls Action.HasTargetSet, passing it the RibbonButton. There is a test in this function, to check if the TargetWithoutContextProperty is set. And it isn't.

It is set in the Action.SetTargetWithoutContext method, which is never called.

Is this normal?

Thanks in advance

Mike

Dec 14, 2010 at 10:38 AM

I've continued to investigate the problem, and the TargetWithoutContext property is the problem.

I've temporarily changed Action.OnTargetWithoutContextChanged so that it calls SetTargetWithoutContext too. And now things are working well.

Is it because I set TargetWithoutContext in a style that I always get UnsetValue in Action.HasTargetSet?

How should I fix that?

Thanks again :)

Dec 14, 2010 at 1:08 PM

Sorry for the late reply.
I'm a little confused: if the action Test is not found in the current action target, it *should* be searched (as far I could remember) in the object reference by DataContext.
Now, you also report that even explicitly setting the TargetWithoutContext doesn't solve the issue... 
The best I can do is to try investigating the issue in your scenario (in particular, with the Ribbon button), to check why the framework doesn't behave as usual (@Rob, please correct me if I'm wrong about the intended behaviour).
Do you have a small repro exibiting the issue? This will greatly help me to look into it shortly. Could you please send it to marco dot amendola at gmail dot com?

Dec 14, 2010 at 2:45 PM

Yes, Test is searched in the DataContext. But DataContext is of type ButtonData, which doesn't have the Test method.

I explicitly set the TargetWithoutContext property in the Style, but as I said, Action.HasTargetSet returns UnsetValue.

I'm trying to make a repro, but it's hard as I have to change of lot of code, because I use postsharp...

Dec 14, 2010 at 3:34 PM

I've just sent a sample to your mail address. Thanks in advance!

Dec 16, 2010 at 12:14 AM
Edited Dec 16, 2010 at 12:24 AM

As you correctly pointed out, setting the Action.Target property through style doesn't affect local value, thus preventing the framework to correctly determine the action handler.
The issue is fixed as of changeset a6b5a421d26c

 

Dec 16, 2010 at 1:55 PM

Thanks for your fix!