WPF Toggle Combo Box Item Text Colour MVVM ReactiveUI

Viewed 178

OK so this is definitely a newbie question that unfortunately could not figure/find the answer to.

Essentially binding a list of objects to a Combobox, when the Disabled property on the object is set to true I want the text colour of the Combobox item to be set to gray.

This is what I have so far:

Combobox item datatype

public class ListItem
{
    public ListItem(string text)
    {
        Text = text;
    }

    public string Text { get; set; }
    public bool Disabled { get; set; }
}

Viewmodel setup


public class MainPageViewModel : ReactiveObject
{
    // In ReactiveUI, this is the syntax to declare a read-write property
    // that will notify Observers, as well as WPF, that a property has 
    // changed. If we declared this as a normal property, we couldn't tell 
    // when it has changed!
    private ListItem _selectedItem;
    public ListItem SelectedItem
    {
        get => _selectedItem;
        set => this.RaiseAndSetIfChanged(ref _selectedItem, value);
    }

    public List<ListItem> Items { get; set; }

    public MainPageViewModel()
    {
        Items = new List<ListItem>
        {
            new ListItem ("A Cat"),
            new ListItem ("A Dog"),
            new ListItem ("A Mouse"),
            new ListItem ("A Frog") { Disabled = true }
        };
    }
}

ReactiveUI Binding

public MainPage()
{
  InitializeComponent();

  ViewModel = new MainPageViewModel();
  
  this.WhenActivated(d =>
  {
    this.OneWayBind(ViewModel, vm => vm.Items, v => v.MyComboBox.ItemsSource)
                    .DisposeWith(d);

    this.Bind(ViewModel, vm => vm.SelectedItem, v => v.MyComboBox.SelectedItem)
                    .DisposeWith(d);

  });
}

Xaml markup

<ComboBox
 Name="MyComboBox"
 Margin="0,0,0,20"
 Foreground="black">
   <ComboBox.ItemTemplate>
     <DataTemplate>
       <TextBlock Text="{Binding}" />
     </DataTemplate>
   </ComboBox.ItemTemplate>

   <ComboBox.ItemContainerStyle>
     <Style TargetType="ComboBoxItem">
       <Style.Triggers>
         <DataTrigger Binding="{Binding Path=Disabled}" Value="True">
           <Setter Property="Foreground" Value="Gray" />
         </DataTrigger>
       </Style.Triggers>
     </Style>
    </ComboBox.ItemContainerStyle>

 </ComboBox>

Any help is appreciated let me know if you need more information.


Solution: It looks like in future I need to test the example code before puttin it up - our actual code had the Disabled property set as a readonly which must mess with WPF binding. Changing it to public set and get solved the first issue of not seeing it greyed out! It would seem staring at a problem for so long blinds you and it really is that simple. As for graying out the selected item I will try it out and see.

3 Answers

The last item in the dropdown already has its text grayed out, so I assume you're asking about the selected item. The ComboBox uses separate data templates for the selected item and the items in the dropdown. You can use a DataTemplateSelector to set both.

public class ComboBoxTemplateSelector : DataTemplateSelector
{
    public DataTemplate SelectedItemTemplate { get; set; }
    public DataTemplate DropdownItemsTemplate { get; set; }

    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        var itemToCheck = container;

        // Search up the visual tree, stopping at either a ComboBox or a ComboBoxItem (or null).
        // This will determine which template to use.
        while (itemToCheck is not null and not ComboBox and not ComboBoxItem)
            itemToCheck = VisualTreeHelper.GetParent(itemToCheck);

        // If you stopped at a ComboBoxItem, you're in the dropdown.
        return itemToCheck is ComboBoxItem ? DropdownItemsTemplate : SelectedItemTemplate;
    }
}
Xaml markup
<StackPanel>
    <StackPanel.Resources>
        <Style x:Key="GrayedOutText" TargetType="TextBlock">
            <Style.Triggers>
                <DataTrigger Binding="{Binding Disabled}" Value="True">
                    <Setter Property="Foreground" Value="Gray" />
                </DataTrigger>
            </Style.Triggers>
        </Style>
        <local:ComboBoxTemplateSelector x:Key="ComboBoxTemplateSelector">
            <local:ComboBoxTemplateSelector.SelectedItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Text}" Style="{StaticResource GrayedOutText}" />
                </DataTemplate>
            </local:ComboBoxTemplateSelector.SelectedItemTemplate>
            <local:ComboBoxTemplateSelector.DropdownItemsTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Text}" Style="{StaticResource GrayedOutText}" />
                </DataTemplate>
            </local:ComboBoxTemplateSelector.DropdownItemsTemplate>
        </local:ComboBoxTemplateSelector>
    </StackPanel.Resources>
    <ComboBox
         Name="MyComboBox"
         Margin="0,0,0,20"
         ItemTemplateSelector="{StaticResource ComboBoxTemplateSelector}">
    </ComboBox>
</StackPanel>

We have some repetition in the DataTemplate definitions, but these tend to grow apart in production code.

Resources

I'm assuming your problem is that ComboBoxItems do not get grayed once the app is running.

I'm not familiar with ReactiveUI, but since I found a problem in your code, I tried it in a CommunityToolkit.Mvvm version of your code and verified my theory.

Bottom of line, you need to implement the ReactiveUI version of INotifyPropertyChanged to the Disabled property.

If you are interested in, I can post the CommunityToolkit.Mvvm version of this code.

Here is an approach that worked in my tests:

Combobox item datatype:

//-- Unchanged
public class ListItem
{
    public ListItem( string text )
    {
        Text = text;
    }

    public string Text { get; set; }

    public bool Disabled { get; set; }
}

Viewmodel setup:

public class MainPageViewModel : INotifyPropertyChanged
{
    private ListItem? _selectedItem;

    public event PropertyChangedEventHandler? PropertyChanged;

    public ListItem? SelectedItem
    {
        get => _selectedItem;
        set
        {
            //-- I didn't had the "RaiseAndSetIfChanged" method, so I just implemented the functionality manually
            if( value != _selectedItem )
            {
                //-- Update the value ...
                _selectedItem = value;

                //-- ... AND inform everyone (who is interested) about the change
                this.PropertyChanged?.Invoke( this, new PropertyChangedEventArgs( nameof( this.SelectedItem ) ) );
            }
        }
    }

    //-- Use always an ObservableCollection when you want to achieve reactivity
    public ObservableCollection<ListItem> Items 
    { get; } = new ObservableCollection<ListItem>();

    public MainPageViewModel()
    {
        //-- Add some test data
        this.Items.Add( new ListItem( "A Cat" ) );
        this.Items.Add( new ListItem( "A Dog" ) );
        this.Items.Add( new ListItem( "A Mouse" ) );
        this.Items.Add( new ListItem( "A Frog" ) { Disabled = true } );

        //-- Just select the first item
        this.SelectedItem = this.Items[0];
    }
}

Main page:

public MainPage()
    {
        //-- Define the DataContext BEFORE the UI will be initialized ;)
        this.DataContext = new MainPageViewModel();

        InitializeComponent();

        //-- Never saw such code before -> just don't do that ;)

        //this.WhenActivated( d =>
        //{
        //    this.OneWayBind( ViewModel, vm => vm.Items, v => v.MyComboBox.ItemsSource )
        //                    .DisposeWith( d );

        //    this.Bind( ViewModel, vm => vm.SelectedItem, v => v.MyComboBox.SelectedItem )
        //                    .DisposeWith( d );

        //} );
    }

Xaml markup:

<DockPanel>

    <ComboBox
        DockPanel.Dock="Top"
        Name="MyComboBox"
        Margin="0,0,0,20"
        Foreground="black"
        ItemsSource="{Binding Items}"
        SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
        <ComboBox.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Text}" />
            </DataTemplate>
        </ComboBox.ItemTemplate>

        <ComboBox.ItemContainerStyle>
            <Style TargetType="ComboBoxItem">
                <Style.Triggers>
                    <DataTrigger Binding="{Binding Path=Disabled}" Value="True">
                        <Setter Property="Foreground" Value="Gray" />
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </ComboBox.ItemContainerStyle>

    </ComboBox>
    
    <!-- Details View -->
    <StackPanel>
        
        <!-- name -->
        <StackPanel Orientation="Horizontal">

            <Label Content="Item Name" />
            <TextBox Text="{Binding SelectedItem.Text}" />

        </StackPanel>

        <!-- disabled flag -->
        <StackPanel Orientation="Horizontal">

            <Label Content="IsDisabled" />
            <CheckBox IsChecked="{Binding SelectedItem.Disabled}" />
        </StackPanel>
    </StackPanel>   
</DockPanel>

I hope this will satisfy your requirements. Have fun :)

Related