Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to databind SelectedItem of RibbonComboBox

My question is basically this one. I thought it would help to provide some more information and code that makes it easier to reproduce the problem, though.

Working with the Microsoft.Windows.Controls.Ribbon.RibbonComboBox from the RibbonControlsLibrary feels like walking through a big bog full of bugs, not something you do if you know a way around it.

Anywho. The biggest problem I encountered was databinding my SelectedItem.

The following is what I started with (after I found out about RibbonGallery?). To have ItemsSource and SelectedItem on subelements of the ComboBox and not even on the same level already gave me the heebie-jeebies, but that seems to be correct.

In the example app, I'm setting the SelectedItem in the constructor of the ViewModel. However, when running the app, no SelectedItem is shown. Even the VS designer is correctly showing "second option"!

Running app:Running App VS designer: Visual Studio Designer

When debugging the SelectedItem setter, you'll notice multiple passes. After setting it the first time to "second option" in the ctor(1, see debug log below), it will reset to null (2) (by external code, I reckon in the control itself). When opening the dropdown in the UI, it will be set to null again (3), then when selecting a value, twice to this value (4,5). I selected "second option", then repeated the procedure with "first option" (6-9). This produced the following log (ignoring the one thousand and one binding exceptions from the ribbon control...):

enter image description here

The big problem obviously is (2), which is resetting my initial selection. Looks like when the control is shown the first time, it is reset. A very ugly workaround would be to set the value by a timer. Setting it in the Loaded event of the user control does work for me in this example app, but in my heavier real-life app, it does not. Anyway, all of that feels wrong. Does anyone know a better solution?

Xaml:

<UserControl x:Class="WpfApplication1.RibbonComboBoxDemo"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:r="http://schemas.microsoft.com/winfx/2006/xaml/presentation/ribbon" 
             xmlns:local="clr-namespace:WpfApplication1"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">

    <UserControl.DataContext>
        <local:ViewModel />
    </UserControl.DataContext>

    <Grid>
        <r:Ribbon >
            <r:RibbonTab Header="First Tab">
                <r:RibbonGroup Header="Group">
                    <r:RibbonComboBox >
                        <r:RibbonGallery SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
                            <r:RibbonGalleryCategory ItemsSource="{Binding Controls}" DisplayMemberPath="Caption" />
                        </r:RibbonGallery>
                    </r:RibbonComboBox>
                </r:RibbonGroup>
            </r:RibbonTab>
            <r:RibbonTab Header="Second Tab" />
        </r:Ribbon>
    </Grid>
</UserControl>

ViewModel:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;

namespace WpfApplication1
{
    public class ViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        public ObservableCollection<ControlBaseModel> Controls { get; private set; }


        private ControlBaseModel _selectedItem;
        public ControlBaseModel SelectedItem { get { return _selectedItem; } set { LogSelectedItemChange(value); _selectedItem = value; OnPropertyChanged("SelectedItem"); } }

        public ViewModel()
        {
            this.Controls = new ObservableCollection<ControlBaseModel>();

            this.Controls.Add(new ControlBaseModel() { Caption = "first option" });
            this.Controls.Add(new ControlBaseModel() { Caption = "second option" });

            this.SelectedItem = this.Controls[1]; // set to second option
        }

        int i = 0;
        private void LogSelectedItemChange(ControlBaseModel value)
        {
            i++;
            string setObject = "null";
            if (value != null)
            {
                setObject = value.Caption;
            }
            Debug.WriteLine(string.Format("{0}: SelectedItem.set(): {1}", i, setObject));
        }

    }

    public class ControlBaseModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        private string _name;
        public string Name { get { return _name; } set { _name = value; OnPropertyChanged("Name"); } }

        private string _caption;
        public string Caption { get { return _caption; } set { _caption = value; OnPropertyChanged("Caption"); } }
    }
}
like image 807
Mike Fuchs Avatar asked Mar 21 '13 18:03

Mike Fuchs


2 Answers

While the View/UserControl loaded event is occuring before the ComboBox SelectedItem is reset to null in my application, the ComboBox loaded event is in fact fired twice, the second time "late" enough. So my current solution, which I will ditch gladly for a better one, is this:

<r:RibbonComboBox>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Loaded">
            <i:InvokeCommandAction Command="{Binding LoadedCommand}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <r:RibbonGallery SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
        <r:RibbonGalleryCategory ItemsSource="{Binding Controls}" DisplayMemberPath="Caption"/>
    </r:RibbonGallery>
</r:RibbonComboBox>

ViewModel:

private ControlBaseModel _lastNonNullSelectedItem;

public ObservableCollection<ControlBaseModel> Controls { get; private set; }

private ControlBaseModel _selectedItem;
public ControlBaseModel SelectedItem 
{ 
    get { return _selectedItem; } 
    set 
    { 
        if (value != null) { _lastNonNullSelectedItem = value; } 
        _selectedItem = value; 
        OnPropertyChanged("SelectedItem"); 
    } 
}
public ICommand LoadedCommand { get; private set; }


public ViewModel()
{
    this.Controls = new ObservableCollection<ControlBaseModel>();
    this.LoadedCommand = new ActionCommand(OnLoaded); // ActionCommand: simple implementation of ICommand

    this.Controls.Add(new ControlBaseModel() { Caption = "first option" });
    this.Controls.Add(new ControlBaseModel() { Caption = "second option" });

    this.SelectedItem = this.Controls[1]; // set to second option
}

private void OnLoaded()
{
    this.SelectedItem = _lastNonNullSelectedItem;
}
like image 129
Mike Fuchs Avatar answered Oct 10 '22 13:10

Mike Fuchs


I ended up just using the standard ComboBox.

<ComboBox SelectedItem="{Binding Item}" ItemsSource="{Binding Items}"/>

If you want the same (very similar) style as the RibbonComboBox, use

<ComboBox SelectedItem="{Binding Item}" ItemsSource="{Binding Items}" IsEditable="True" IsReadOnly="True"/>
like image 41
Nick Strupat Avatar answered Oct 10 '22 12:10

Nick Strupat