Style SelectedItem in ListBox upon Load WPF - c#

I have a ListBox, as such:
<ListBox
//other stuff
ItemsSource="{Binding ViewModels, UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding SelectedThing, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="Purple" />
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
ViewModel.cs:
public SomeType SelectedThing
{
get => selectedThing;
set => //set with INotifyPropertyChanged
}
public ObservableCollection<SomeType> ViewModels
{
get => viewModels;
set => //set with INotifyPropertyChanged
}
It's possible that SelectedThing is defined in the ViewModel when loading the app, so I want it to be styled accordingly.
It works great when I open the app and then click on an item, but how can the style be applied on load?
I tried with:
<Style.Triggers>
<DataTrigger Binding="{Binding IsEnabled, RelativeSource={RelativeSource Mode=Self}}" Value="True">
<Setter Property="Background" Value="Purple" />
</DataTrigger>
</Style.Triggers>
But every item of the ListBox is enabled, so it applies it to every item upon load.
EDIT:
After debugging a bit, I found out that when setting SelectedThing on load, SelectedItem remains null.
EDIT:
Here is the OnLoaded method in the ViewModel, where I am setting SelectedThing if the user has selected it while previously using the app. The purpose is to keep the selection after closing and reopening the app.
public IAsyncRelayCommand OnLoadedCommand { get; set; }
In the constructor:
OnLoadedCommand = new AsyncRelayCommand(OnLoaded);
In the View:
<b:Interaction.Triggers>
<b:EventTrigger EventName="Loaded">
<b:InvokeCommandAction Command="{Binding OnLoadedCommand}" />
</b:EventTrigger>
</b:Interaction.Triggers>
The actual method:
public async Task OnLoaded()
{
//other stuff
if (App.Current.Properties.Contains(nameof(SelectedThing)))
{
var selected = JsonConvert.DeserializeObject<SomeType>(App.Current.Properties[nameof(SelectedThing)].ToString());
SelectedThing = selected;
}
}

The reason why there is no item selected is because you are setting the SelectedThing source property to a value that's not in the ViewModels source collection.
As you have already discovered, this works better:
var selectedViewModel = ViewModels.Where(x => x.SelectedThing == selected.SelectedThing).FirstOrDefault();
SelectedThing = selectedViewModel;
The item to be selected must be present in the source collection.

Related

How do I bind a WPF combo box to a different list when the dropdown is open?

I have several combo boxes in a Scheduling module that all have dropdown lists based on an "Active" field.
public class Project
{
public int ProjectID { get; set; }
public int ProjectTitle { get; set; }
public bool Active { get; set; }
}
<ComboBox
Name="ProjectComboBox"
ItemsSource="{Binding AllProjects}"
SelectedItem="{Binding Project, Mode=TwoWay}">
</ComboBox>
The calendar's editing form must always display legacy information in its combo boxes, even if a particular item in a combo list has been deactivated. But if the drop-down is opened, it must only show those items in the list that are still active.
How would I accomplish this?
I have tried this, in the codebehind:
private void ProjectComboBox_DropDownOpened(object sender, EventArgs e)
{
ProjectComboBox.SetBinding(ItemsControl.ItemsSourceProperty, "ActiveProjects");
}
private void ProjectComboBox_DropDownClosed(object sender, EventArgs e)
{
ProjectComboBox.SetBinding(ItemsControl.ItemsSourceProperty, "AllProjects");
}
Which displays the correct list in the dropdown, but de-selects the originally-selected Project. If the user does not select a new project, the combo box needs to retain its original selection when the dropdown is closed.
instead of changing ItemsSource, hide inactive elements via Visibility binding:
<BooleanToVisibilityConverter x:Key="boolToVisibility"/>
<ComboBox Name="ProjectComboBox"
ItemsSource="{Binding AllProjects}"
DisplayMemberPath="ProjectTitle"
SelectedItem="{Binding Project, Mode=TwoWay}">
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Setter Property="Visibility"
Value="{Binding Active, Converter={StaticResource boolToVisibility}}"/>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
This also works, and might provide better flexibility for those looking to do something similar:
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Setter Property="Visibility" Value="Visible"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Active}" Value="False">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.ItemContainerStyle>

Changing content of GroupBox based on ComboBox selection

I have a ComboBox containing four int values, 1, 2, 3, 4. Below that, I have a GroupBox. What I'm trying to do is display unique content in the GroupBox based on the values of the ComboBox; i.e. maybe a TextBlock or TextBox for 1 and a Button for 2.
Currently, I have four separate GroupBoxes all on top of each other and have the Visibility property of the those GroupBoxes bound to unique bool properties in the ViewModel which I set whenever the SelectedItem of the ComboBox changes. But I feel like there's a way where I can condense this and just keep one GroupBox and fill it with the correct content instead of having four separate ones and four separate IsVisible properties.
<ComboBox ItemsSource="{Binding PackageTypes}" SelectedItem="{Binding SelectedType}" />
<GroupBox /><!--Change content of this based on combobox's selection-->
public class MainViewModel : ViewModelBase
{
private PackageModel_selectedPackageModel;
public PackageModel SelectedPackageModel
{
get => _selectedPackageModel;
set
{
_selectedPackageModel = value;
}
}
public ObservableCollection<int> PackageTypes { get; set; }
public int SelectedType { get; set; }
public MainViewModel()
{
PackageTypes = new ObservableCollection<int>() { 1, 2, 3, 4 };
}
}
You may use a Style with triggers, e.g.:
<ComboBox x:Name="cmb" xmlns:s="clr-namespace:System;assembly=mscorlib">
<s:Int32>1</s:Int32>
<s:Int32>2</s:Int32>
<s:Int32>3</s:Int32>
<s:Int32>4</s:Int32>
</ComboBox>
<GroupBox Header="...">
<GroupBox.Style>
<Style TargetType="GroupBox">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedItem, ElementName=cmb}" Value="1">
<Setter Property="Content">
<Setter.Value>
<TextBlock Text="1..." />
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding SelectedItem, ElementName=cmb}" Value="2">
<Setter Property="Content">
<Setter.Value>
<Button Content="2..." />
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</GroupBox.Style>
</GroupBox>
Instead of binding to the SelectedItem property of the ComboBox, you may of course bind to a source property of the view model:
<DataTrigger Binding="{Binding SelectedType}" Value="1">
Make sure that the view model implements the INotifyPropertyChanged interface and raises change notifications.

WPF Using bindings to ViewModel in style used to change controls

The objective is to swap the control at runtime based on a property in my ViewModel, and have the displayed control be have a binding that updates the properties in the ViewModel. I started by creating the following Style in the View.xaml:
<UserControl.Resources>
<Style x:Key="DisplayTextOrButton" TargetType="{x:Type ContentControl}">
<Style.Triggers>
<DataTrigger Binding="{Binding TextNotButton}" Value="True">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<TextBox Content="{Binding SomeText}"/>
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding TextNotButton}" Value="False">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Button Content="{Binding ButtonText}"/>
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
Note that the bindings LabelText and ButtonText are the bindings to the properties in the ViewModel.
Then further on in the View.xaml I have the following:
<ContentControl Content="{Binding TextNotButton}"
Style="{StaticResource DisplayTextOrButton}">
</ContentControl>
Finally, the ViewModel.cs has the following properties:
private bool textNotButton;
public bool TextNotButton
{
get => this.textNotButton;
set
{
this.textNoButton = value;
this.OnPropertyChanged("TextNotButton");
}
}
private string someText;
public string SomeText
{
get => this.someText;
set
{
this.someText = value;
this.OnPropertyChanged("SomeText");
}
}
private string buttonText;
public string ButtonText
{
get => this.buttonText;
set
{
this.buttonText = value;
this.OnPropertyChanged("ButtonText");
}
}
The style works well for swapping between the label and the button, but changing the text in the TextBox does not update the property in the ViewModel, and the Button's text is empty (I imagine because the binding hasn't worked)
I believe this is because the style is a static resource so the bindings SomeText and ButtonText in the style aren't actually the bindings in the ViewModel, but I'm not sure how to pass the reference of the other properties into the style. Or even if that's a thing. I'm pretty new to XAML so not sure on how to handle this

How to visually show multiselect on TreeView in WPF

I'm attempting to enable a TreeView control to support multi select.
The very basic flow works, if you select multiple items in the TreeView while holding down ctrl or shift then it will successfully add those items to a list I have in the view model.
The problem is that when actually clicking on the TreeView items it will only select one visually i.e. only one item is marked as selected. How can I make it highlight/mark multiple items? I don't understand where this is controlled.
The TreeView xaml:
<TreeView x:Name="availableColumnsTreeView"
AutomationProperties.AutomationId="availableColumnsTreeView"
x:Uid="availableColumnsTreeView"
SelectedItemChanged="availableColumnsTreeView_SelectedItemChanged"
ItemsSource="{Binding Path=TreeFieldData, Mode=OneWay, Converter={StaticResource SortingConverter}, ConverterParameter='DisplayName.Text'}"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Grid.Row="0">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate x:Uid="HierarchicalDataTemplate_1" ItemsSource="{Binding Path=Children, Mode=OneWay, Converter={StaticResource SortingConverter}, ConverterParameter='DisplayName.Text'}">
<TextBlock x:Uid="TextBlock_1" Text="{Binding DisplayName.Text, Mode=OneWay}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
So "availableColumnsTreeView_SelectedItemChanged" is invoked fine, but I need it to actually highlight the selected items.
EDIT: Please read my question before marking it as a duplicate. I tried to be as specific as possible to what my problem is. I'm not looking for a whole solution for multi select hidden away in some one drive document.
I'm not sure if I 100% follow you. Could provide a small example please?
Sure.
Here is xaml:
<TreeView ItemsSource="{Binding Items}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=Children, Mode=OneWay}">
<CheckBox Content="{Binding Text, Mode=OneWay}" IsChecked="{Binding IsSelected}">
<CheckBox.Style>
<Style TargetType="CheckBox">
<Style.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<DataTrigger.Setters>
<Setter Property="Foreground" Value="Red" />
</DataTrigger.Setters>
</DataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
</CheckBox>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
And here is cs:
public class Item : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public string Text { get; set; }
public List<Item> Children { get; set; }
bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsSelected)));
}
}
public Item(string text)
{
Text = text;
}
}
public partial class MainWindow : Window
{
public List<Item> Items { get; set; } = new List<Item>
{
new Item("1") { Children = new List<Item>
{
new Item("11"),
new Item("12"),
new Item("13"),
}},
new Item("2") { Children = new List<Item>
{
new Item("11"),
new Item("12"),
new Item("13"),
}},
new Item("3"),
};
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
}
I am using CheckBox to select item (no idea how you do it). If item is selected it has its foreground changed to red via data trigger.
As you can see selection (disregards how you implement it, really, I am using single selection TreeView) is stored inside items as IsSelected value. You can traverse hierarchical collection to get a list of selected items (this is called flattering).
Note: IPropertyChanged, it's required if you plan to set IsSelected from code-behind (e.g. select all items on button press).
It should be easy to adapt to your case.
How can I make it highlight/mark multiple items? I don't understand where this is controlled.
You define the appearance of a TreeViewItem container using a TreeViewItem style. If you add an "IsSelected" property to your data object that keeps track of whether the item is currently selected, you could use a DataTrigger that binds to this one and provide the highlighting, e.g.:
<TreeView>
<TreeView.Resources>
<Style TargetType="TreeViewItem">
<Style.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter Property="Background" Value="Yellow" />
</DataTrigger>
</Style.Triggers>
</Style>
</TreeView.Resources>
...
</TreeView>
Make sure that the data class where the "IsSelected" property is defined implements the INotifyPropertyChanged interface and that you set this property in your event handler or command.

Highlight one item in DataGrid

I'm displaying Cars in a DataGrid and would like to highlight one special car, the CurrentlySelectedCar.
When the user double clicks on a car, this car is saved as CurrentlySelectedCar in my MainViewModel. Now, whenever the user comes back to the DataGrid, I would like to highlight this car = row, e.g. by using a red background.
I have found out how to highlight rows in a DataGrid based on certain values, but in my case, all I have is the CurrentlySelectedCar.
My First try:
<Style TargetType="DataGridRow">
<Style.Triggers>
<!-- not working-->
<DataTrigger Binding="{Binding CurrentlySelectedCar}" >
<Setter Property="Background" Value="Red"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
My second try:
<Style TargetType="DataGridRow">
<Style.Triggers>
<!-- not working either, "Binding can only be set on DependencyProperty of DependecyObject"-->
<DataTrigger Binding="{Binding Guid}" Value="{Binding CurrentlySelectedCar.Guid}" >
<Setter Property="Background" Value="Red"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
How can I highlight the row with this information?
I think that you have to do something like this described in this answer: Using bindings in DataTrigger condition
<Style TargetType="DataGridRow">
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource someMultiConverter}">
<Binding Path="Guid"></Binding>
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Datagrid}}" Path="CurrentlySelectedCar.Guid"></Binding>
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Background" Value="Red"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
You have to write a multiconverter that return true if the two Guid are equals.
Try the following instead, since each datagrid row has an IsSelected property, you can bind to it directly.
<DataGrid EnableRowVirtualization="False">
<DataGrid.Resources>
<Style TargetType="DataGridRow">
<Style.Triggers>
<DataTrigger Binding="{Binding IsSelected}" >
<Setter Property="Background" Value="Red"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.Resources>
</DataGrid>
Updated response
The problem right now is your DataContext is the DataGridRow, and it doesn't have access to the MainViewModel. You can address this by passing the DataGridRow to a converter that is aware of the current MainViewModel. However, it is a lot of code, and is barely comprehensible.
<Window>
<Window.Resources>
<local:IsCurrentlySelectedCarConverter x:Key="IsCurrentlySelectedCarConverter" />
</Window.Resources>
...
<DataTrigger Binding="{Binding
Path=DataContext,
Converter={StaticResource IsCurrentlySelectedCarConverter}}" >
<Setter Property="Background" Value="Red" />
</DataTrigger>
</Window>
Here's corresponding converter
public class IsCurrentlySelectedConverter : IValueConverter
{
public MainViewModel MainViewModel { get; set;}
public object Convert(object value, ....)
{
return (value == MainViewModel.CurrentlySelectedCar);
}
}
and you'll need to wire in the converter's MainViewModel manually in your view
this.Resources["IsCurrentlySelectedCarConverter"].MainViewModel = _mainViewModel;
and at this point you'd have to question the maintainability of the monster that has been created.
It may be better to replace each Car with a CarViewModel so that it has a property called IsSelected and you can maintain that in code. The following in my opinion is easier to follow what is actually going on.
public class CarViewModel : INotifyPropertyChanged
{
public MainViewModel { get; set; }
public bool IsSelected { get { return this == MainViewModel.CurrentlySelectedCar; } }
// Call RaisePropertyChanged("IsSelected") whenever
// CurrentlySelectedCar is changed
public void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName);
}
}

Categories

Resources