5

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.

ree6
  • 383
  • 2
  • 13
  • Do you change the `Disabled` property at runtime? As far as I can tell you do not implement the `INotifyPropertyChanged` interface to notify any changes. Furthermore, you say that you want to change the color to `Gray` when `Disabled` is `true`, but in your XAML, it triggers on `False`. Does it work if you create an item with its `Disabled` property initially set to the trigger value? – thatguy Aug 25 '22 at 12:31
  • @thatguy sorry I should have been more clear. When the ViewModel gets build we essentially build a `List` and some of those items will have the `Disabled` property set to `true`. And to the second part that is a typo I was messing around with to try and get it to work - your right it should be `Value="True"` I will update the code. – ree6 Aug 25 '22 at 14:07
  • Were you able to solve your problem? – Funk Sep 06 '22 at 09:41

3 Answers3

4

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
Funk
  • 10,976
  • 1
  • 17
  • 33
  • Thanks - and you were right my code should just have worked. As I updated my post the difference between the example I posted and the product code was the `Disabled` property was a `readonly`. Doh!!! Thanks again. Will try out the SelectedItem code next – ree6 Sep 13 '22 at 13:49
1

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.

Andrew KeepCoding
  • 7,040
  • 2
  • 14
  • 21
0

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 :)

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Sandman
  • 302
  • 2
  • 14
  • Thanks will take a look. The code you commented out btw is the ViewModel binding code for ReactiveUI - thats pretty important to how it works. – ree6 Sep 13 '22 at 13:43
  • I never was in touch with ReactiveUI before ... but it looks quite queer. Following the MVVM pattern, there is no need to place code in the code-behind file. I would not recommend to bind all properties manually in the code-behind while you can archieve this easily within the xaml markup. In my opinion it looks like an anti-pattern :/ – Sandman Sep 15 '22 at 10:05