简体   繁体   中英

WPF Toggle Combo Box Item Text Colour MVVM ReactiveUI

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.

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 ComboBoxItem s 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 try 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 archieve 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:)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM