Conditional binding from view model to view in WPF using MVVM pattern - c#

I am developing a WPF application using MVVM pattern. I have a combo in the view and two lists in the viewmodel (projects and organizations). Depending on the organizations list items I have to bind the the name of the organization or not.
For example if the Count property of the organizations list is 1 the combobox item have to be "ProjectName", and if the Count property of the organizations list is greater than 1 the combobox item should look like "ProjectName - OrganizationName".
This is the XAML code I have:
<ComboBox x:Name="textBox3" Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" ItemsSource="{Binding Path=Projects}" DisplayMemberPath="Name" IsSynchronizedWithCurrentItem="True" SelectedItem="{Binding Path=SelectedProject}">
</ComboBox>
How should I achieve this purpose. I hope for a little help. Cheers.
I added the property projectFullName in the viewmodel but I got an empty combobox:
public string ProjectFullName
{
get
{
if (this.organizations.ToList().Count > 1)
{
this.projectFullName = string.Format("{}{0} - {1}", this.selectedProject.Name, this.organizations.First(org => org.Id == this.selectedProject.OrganizationId).Name);
}
else if (this.organizations.ToList().Count == 1)
{
this.projectFullName = this.selectedProject.Name;
}
return this.projectFullName;
}
}
XAML code:
<ComboBox x:Name="textBox3" Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" ItemsSource="{Binding Path=Projects}" DisplayMemberPath="{Binding Path=ProjectFullName}" IsSynchronizedWithCurrentItem="True" SelectedItem="{Binding Path=SelectedProject}">
</ComboBox>

you have several options to implement this, but in my opinion the best is:
Add a property to your Data Context, the will be called "FullName" or something.
That will return: (Pseudo)
if Projects count > 0 then
return Name + '-' + ProjectName
else return Name
then bind DisplayMemberPath to FullName.

Datatrigger is indeed your friend. Make sure the ComboBox does not set the DisplayMemberPath, because that will override the style setters.
<Style x:Key="MyStyle" TargetType="ComboBox">
<Setter Property="DisplayMemberPath" Value="DefaultName"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Items.Count}" Value="1">
<Setter Property="DisplayMemberPath" Value="OtherName"/>
</DataTrigger>
</Style.Triggers>
</Style>

Related

TreeViewItem styles not being applied when selecting an item dynamically over via input

I have a treeview dynamically generated within the program. It uses properties on the class to select items by default if the user sets the preference for it:
However, when I do this, it applies the default style, rather than the current style, which is currently set and applies a AdonisUI dark mode style if requested, or light if not.
The Tree View (and Style) code:
<Window.Resources>
<Color x:Key="TitleBarColor">#FF191970</Color>
<Color x:Key="TitleBarForeColor">#FFFFFAF0</Color>
<Style x:Key="SystemTreeViewItemStyle" TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</Window.Resources>
....
<TreeView Name="tvwSystemTree" Grid.Column="0" Grid.Row="0"
SelectedItemChanged="tvwSystemTree_SelectedItemChanged" ItemContainerStyle="{StaticResource SystemTreeViewItemStyle}"
Visibility="Hidden">
<TreeView.Style>
<Style TargetType="TreeView" BasedOn="{StaticResource {x:Type TreeView}}"/>
</TreeView.Style>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate
DataType="{x:Type lAWObjects:SystemObject}" ItemsSource="{Binding Items}">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding IconUri, Mode=OneWay}" Height="16" Width="16" />
<TextBlock Text="{Binding Title}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
And in code-behind:
ObservableCollection<SystemObject> AstralObjects = new();
SystemObject root = new SystemObject() { Title = ourSystem.SystemName, IconUri = new Uri(SystemObject.SystemLogo) };
foreach(Star v in ourSystem.SystemStars)
{
SystemObject child = new SystemObject() { Title = v.Name, IconUri = new Uri(SystemObject.SunLogo) };
foreach (IOrbitalBody p in ourSystem.PlanetaryBodies)
{
if (p.Parent == v)
{
SystemObject child2 = new SystemObject() { Title = p.Name, IconUri = new Uri(SystemObject.PlanetLogo) };
child.Items.Add(child2);
}
}
root.Items.Add(child);
}
tvwSystemTree.ItemsSource = AstralObjects;
tvwSystemTree.Visibility= Visibility.Visible;
grdDetailView.Visibility = Visibility.Visible;
if (preferences.AutoDisplaySystem)
{
foreach (var v in AstralObjects)
{
if (v.Title == ourSystem.SystemName)
{
v.IsSelected = v.IsExpanded = true;
tvwSystemTree_SelectedItemChanged(this, new RoutedPropertyChangedEventArgs<object>(null, v));
}
}
}
For completion's sake, the SystemObject code that is probably most relevant is that it implements INotifyPropertyChanged. But I can provide it as well if requested.
When this code fires, it applies the normal blue-background and white-text. But if you click any option in the tree, it then applies the style specified colors.
I've tried specifiying that <Style x:Key="SystemTreeViewItemStyle" TargetType="{StaticResource {x:Type TreeViewItem}}"> but it appears AdonisUI doesn't support those properties on it. (And a code-search on github also appears to verify this.)
My only guess is that somehow the selection style is only applied on user interaction. Is there a way around this that I haven't figured out? I'm rather reluctant to apply explicit style colors so I don't have to create variations for any style I may apply in the future.
Update After some investigation I've found out it's because it's overriding Adonis's code (which makes sense) even if I attempt to apply it via {StaticResource {x:Key TreeViewItem}}, but is not respecting any changes I attempt to make via specified dynamic resource.

Binding ComboBoxItem Value Inside of a Setter Property in WPF MVVM

In my wpf application I have a ComboBox which I want to have the ability to disable the selection of items in the drop-down programmatically. The issue that I am having is that the binding ComboBoxItemIsEnabled is not working as expected inside the setter. If remove the binding and use either True or False it works as expected.
XAML
<ComboBox
ItemsSource="{Binding Path=ConfigItems.Result}"
DisplayMemberPath="Name"
IsEditable="True"
FontSize="14"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
IsTextSearchEnabled="False"
Text="{Binding Path=ConfigItem,
UpdateSourceTrigger=LostFocus,
TargetNullValue={x:Static sys:String.Empty}}"
b:ComboBoxBehaviors.OnButtonPress="True">
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Setter Property="IsEnabled" Value="{Binding ComboBoxItemIsEnabled}" />
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
C#
private string _comboBoxItemIsEnabled = "True";
public string ComboBoxItemIsEnabled
{
get
{
return this._comboBoxItemIsEnabled;
}
set
{
this.SetProperty(ref this._comboBoxItemIsEnabled, value);
}
}
public async Task<ConfigItem[]> LoadConfigItemsAsync(string partialName)
{
try
{
if (partialName.Length >= 5)
{
this.ComboBoxItemIsEnabled = "True";
return await this._Service.GetConfigItemsAsync(partialName);
}
this.ComboBoxItemIsEnabled = "False";
return new[] { new ConfigItem("Minimum of 5 characters required", null)};
}
catch (Exception)
{
this.ComboBoxItemIsEnabled = "False";
return new[] { new ConfigItem("No results found", null) };
}
}
I also get the the following error from the debug console when the ComboBoxIsEnabled is being set.
System.Windows.Data Error: 40 : BindingExpression path error: 'ComboBoxItemIsEnabled' property not found on 'object' ''ConfigItem' (HashCode=56037929)'. BindingExpression:Path=ComboBoxItemIsEnabled; DataItem='ConfigItem' (HashCode=56037929); target element is 'ComboBoxItem' (Name=''); target property is 'IsEnabled' (type 'Boolean')
I am using the same mvvm method to target an IsEnabled property for a button else where without an issue. The only difference I can see in the issue above is that I am setting the property within a setter instead.
Many thanks for any wisdom you can part with on how to solve this issue.
After much procrastinating and smashing my head against the keyboard I managed to come to a solution. As it turns out I needed to set the Relative source for the binding. Because I didn't define the DataContext for my solution, every time I pressed a character in the combobox the ItemSource was updated. This meant the ComboBoxItemIsEnabled binding couldn't be found giving me the error above. Below is my updated code, I have added DataContext in front of my binding and added RelativeSource={RelativeSource AncestorType=ComboBox} behind it.
Below is my final code.
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Setter Property="IsEnabled" Value="{Binding DataContext.ComboBoxItemIsEnabled, RelativeSource={RelativeSource AncestorType=ComboBox}}" />
</Style>
</ComboBox.ItemContainerStyle>

WPF checkbox all not not updating on one checkbox row is unchecked

I had a checkbox all column inside the datagrid in WPF C#.
<DataGridCheckBoxColumn Binding="{Binding IsSelected,UpdateSourceTrigger=PropertyChanged}" CanUserSort="False">
<DataGridCheckBoxColumn.ElementStyle>
<Style TargetType="CheckBox" BasedOn="{StaticResource {x:Type CheckBox}}">
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
</Style>
</DataGridCheckBoxColumn.ElementStyle>
<DataGridCheckBoxColumn.HeaderTemplate>
<DataTemplate x:Name="dtAllChkBx">
<CheckBox Name="cbxAll" HorizontalAlignment="Center" Margin="0,0,5,0" IsEnabled="{Binding Path=DataContext.IsCbxAllEnabled,RelativeSource={RelativeSource AncestorType=DataGrid}}"
IsChecked="{Binding Path=DataContext.AllSelected,RelativeSource={RelativeSource AncestorType=DataGrid},UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</DataGridCheckBoxColumn.HeaderTemplate>
</DataGridCheckBoxColumn>
When I check the All checkbox, of course, it will mark all the checkboxes, but once I uncheck one checkbox, the All checkbox is still checked. This should be unchecked. How should I do that using WPF C#.
If I understood you correctly - after any change of IsSelected property inside collection item you should update AllSelected value.
So, you need some callback inside all your items(event or Action or any mechanism you want) and change get logic for AllSelected
Here is some draft for item IsSelected property and constructor:
public bool IsSelected {
get { return isSelected; }
set {
isSelected = value;
OnPropertyChanged();
if (globalUpdate != null) globalUpdate();
}
}
public ItemClass(Action globalUpdate, ...your parameters) {
this.globalUpdate = globalUpdate;
...do smth with your parameters
}
Example of usage:
new ItemClass(() => OnPropertyChanged("AllSelected"))
And of course don't forget about AllSelected getter
public bool AllSelected {
get { return YourGridItemsCollection.All(item => item.IsSelected); }
Now when you check manually all items then AllSelected will be automatically checked, and unchecked when you uncheck any item.

DataGridComboBoxColumn binding to a list of custom class

<DataGridComboBoxColumn x:Name="categoryColumn" Header="Category"
SelectedValueBinding="{Binding CategoryID}"
SelectedValuePath="CategoryID"
DisplayMemberPath="CategoryName"
Width="200">
categoryColumn.ItemsSource = FetchData.CategoriesList;
List<FileModel> _files = new List<FileModel>();
_files.Clear();
_files.Add(new FileModel
{
Filename = "Test.pdf",
Title = "Test",
Category = new CategoryModel
{
CategoryID = 63,
CategoryName = "Personal"
}
});
DataGrid.ItemsSource = _files;
Being new to WPF I am unable to bind the data/item source to DataGridComboboxCOlumn. Here combo box is not at all visible.
Please help.
The problem is that the dataContext of the DataGrid isn't being passed on to the DataGridComboBoxBolumn.. because they aren't part of the same visual tree.
So... when you try to bind to the value of the CategoryModel within the DataGrid... it cannot find it.
Here is one approach to this problem, which uses ElementStyles to forward the dataContext by making the Column part of the same visual tree as the DataGrid:
<!—now itemssource will find the correct DataContext-->
<dg:DataGridComboBoxColumn Header="Current Product"
SelectedValueBinding="{Binding Path=CurrentProduct}"
SelectedValuePath="ProductID"
DisplayMemberPath="ProductName">
<dg:DataGridComboBoxColumn.ElementStyle>
<Style TargetType="ComboBox">
<Setter Property="ItemsSource" Value="{Binding Path=ProductsInCategory}" />
</Style>
</dg:DataGridComboBoxColumn.ElementStyle>
<dg:DataGridComboBoxColumn.EditingElementStyle>
<Style TargetType="ComboBox">
<Setter Property="ItemsSource" Value="{Binding Path=ProductsInCategory}" />
</Style>
</dg:DataGridComboBoxColumn.EditingElementStyle>
</dg:DataGridComboBoxColumn>
You can use this approach, just make your CategoriesList a property that you can bind to:
public ObservableCollection<CategoryModel> CategoriesList { get; set; }
Then in your setup code:
CategoriesList = FetchData.CategoriesList;
(So in the above example, you would be binding the ItemsSource of your ComboBox to "CategoriesList", instead of "ProductsInCategory")

WPF : ListBox SelectedItems not updating or being accumulated when selection changed

I meet the strange phenomenon. If I did a lot of Web Search, I can't get a solution.
I use WPF, VS2010 engine. The GUI mainly consists of four ListBoxes.
First ListBox is aggregation of Mart Branches, Second ListBox is aggregation of goods as fruit, meat, vegetable ans so on that a Mart sells.
Third ListBox is a kind of Mart goods. (apple, peach, peer, melon...) 4th ListBox is aggregation of a kind of selected fruit, for example, if Apple is selected, cortland, crabapple, sansa, gala and so on.
On starting Program, All Mart Branch is displayed on 1'st ListBox, and if I select one branch, Goods list that is sold at Mart is displayed on 2;nd listbox.
In the same way, a sub-kind of selected item is displayed on 3'rd ListBox and 4'th ListBox.
1'st, 2'nd, 4'th ListBox do good, But 3'rd ListBox have error. I think 2'nd and 3'rd have same structure.
3'rd ListBox cannot update selecteditem change. Regardless of SelectionMode (Single, Multiple, Extend), SelectedItems of 3'rd ListBox have all items
I have selected. Furthermore, 3'rd ListBox.SelectedItems contains duplicated item.
But, SelectionChanged event firing is good. Only, SelectedItems or SelectedItem is problem.
Currently, to make a this function, I use detour way. After fire SelectionChanged, I catch AddedItems of SelelctionChangedEventArgs.
So, I use AddedItems instead of SelectdItem like SelectionMode = "Single"
I tried many suggestion, VirtualizingStackPanel.IsVirtualizing="False", IsSynchronizedWithCurrentItem="True", But I cannot find solution.
I'm sorry I cannot serve all behindcode and ~ xaml. Actually, This application is very big. So, I can't do that.
And, I'm sorry my English ability is poor.
Branches
<StackPanel Orientation="Vertical" Grid.Column="0">
<Label HorizontalAlignment="Center">Goods</Label>
<ListBox Name="lbLoadedGoods" Height="120" Margin="2" SelectionMode="Single" SelectionChanged="lbLoadedGoods_SelectionChanged"></ListBox>
</StackPanel>
<StackPanel Orientation="Vertical" Grid.Column="1" >
<Label HorizontalAlignment="Center">ITEM</Label>
<!-- ListBox Double Click Event Setter -->
<ListBox Name="lbLoadedItems" Height="120" Margin="2" SelectionMode="Single" SelectionChanged="lbLoadedItems_SelectionChanged">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem }}">
<EventSetter Event="MouseDoubleClick" Handler="lbLoadedItems_MouseDoubleClick"></EventSetter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</StackPanel>
</Grid>
</GroupBox>
<Label Grid.Column="0" Grid.Row="0" HorizontalAlignment="Center">SubItem</Label>
<ListBox Name="lbSelectedSubItemData" Height="80" Grid.Column="0" Grid.Row="1" Margin="4">
<!-- ListBox Double Click Event Setter -->
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
<EventSetter Event="MouseDoubleClick" Handler="lbSelectedSubItemData_MouseDoubleClick"></EventSetter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</StackPanel>
// public ObservableCollection<string> BranchList { get; private set; }
// public ObservableCollection<Goods> GoodList { get; private set; }
// public ObservableCollection<Items> ItemList { get; private set; }
// private ObservableCollection<string> m_usbitemlist = new ObservableCollection<string>();
// public ObservableCollection<string> SubItemList { get { return m_usbitemlist; } private set { m_usbitemlist = value; } }
private void BindFabFileList()
{
lbBranches.ItemsSource = BranchList;
lbLoadedGoods.ItemsSource = GoodList;
lbLoadedItems.ItemsSource = ItemList;
}
private void lbLoadedGoods_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ItemList = new ObservableCollection<Items>();
// ItemList Add. From Selected Goods
}
You're setting the reference to the object ItemList to a new instance, rather than updating the items on the list that WPF is already hooked up to. WPF automatically hooks up to the CollectionChanged event of ObservableCollection, but it does not know that you have changed the reference in the code behind. The UI is still connected to the old object in memory.
Instead of:
ItemList = new ObservableCollection<Items>();
ItemList.Add(...);
Do this:
ItemList.Clear();
ItemList.Add(...);

Categories