ComboBox not autoselecting initial value - c#

I have a ComboBox bound to a list of people in a simple ViewModel. The SelectedPerson is set in the constructor of the ViewModel, but when I run the application, the ComboBox is not set with its initial value. What am I doing wrong?
Notice that two instances of the MyPerson class should be considered equal when they have the same Id.
Unfortunately, I cannot modify the MyPerson to override Equals (it's third party).
The only option I've seen so far is to user the Adapter pattern to wrap the instances of this class and implement a custom Equals method there.
I feel that there should be a better method that is native to WPF to match items from a list by some kind of "key". In my case, the list of items and the selected item come from different sources, and that's why they have an Id property acting as a primary key.
I've played with SelectedValue and SelectedValuePath, but nothing works.
<Window x:Class="Test.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Test"
mc:Ignorable="d"
x:Name="Root"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<vm:MainViewModel />
</Window.DataContext>
<ComboBox ItemsSource="{Binding People}" SelectedItem="{Binding SelectedPerson}" SelectedValuePath="Id"
DisplayMemberPath="Name"
SelectedValue="{Binding SelectedPerson}" />
</Window>
And this ViewModel as DataContext:
public class MainViewModel
{
public MainViewModel()
{
SelectedPerson = new MyPerson { Name = "Mary", Id = 1 };
}
public MyPerson SelectedPerson { get; set; }
public IEnumerable<MyPerson> People { get; } = new List<MyPerson>()
{
new MyPerson() {Name = "Mary", Id = 1 },
new MyPerson() {Name = "John", Id = 2 },
};
}
public class MyPerson
{
public string Name { get; set; }
public int Id { get; set; }
}

The problem is that the new object you create here
SelectedPerson = new MyPerson { Name = "Mary", Id = 1 };
isnt' the same as in your list so the Equals method would return False in all cases!
As someone else already suggested, you have to get the real object that is in the list by doing this:
public MainViewModel(){
SelectedPerson = People.First(x=> x.Name.Equals("Mary")); //Or: x.Id == 1
}
But there is also another solution: You can override the Equals function in your MyPerson class, so that every MyPerson instance with the same Name and/or Id would indeed be seen as the same person.
EDIT
As you're using ViewModels in your project, it would be better if you had a ViewModel for the MyPersonclass too. It would solve your problems and make your design better!

Change
public MainViewModel()
{
SelectedPerson = new MyPerson { Name = "Mary", Id = 1 };
}
To
public MainViewModel()
{
SelectedPerson = People.ElementAt(0);
}
Or if ou want by name:
public MainViewModel()
{
SelectedPerson = People.First(x=> x.Name=="Mary");
}

Related

Why combobox not select any value?

Here is the code of XAML:
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<ComboBox x:Name="CB" SelectedValue="{Binding Model,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" DisplayMemberPath="Value" VerticalAlignment="Center">
</ComboBox>
</Grid>
</Window>
And here is the code of code-behind:
public partial class MainWindow : Window
{
public List<TestModel> Models { get; set; } = new List<TestModel>();
TestModel _Model = new TestModel() { Key = "Joe", Value = "456" };
public TestModel Model
{
get => _Model; set
{
if (_Model != value)
{
_Model = value;
}
}
}
public MainWindow()
{
InitializeComponent();
Models.Add(new TestModel() { Key = "John", Value = "123" });
Models.Add(new TestModel() { Key = "Joe", Value = "456" });
Models.Add(new TestModel() { Key = "Kay", Value = "547" });
Models.Add(new TestModel() { Key = "Rose", Value = "258" });
CB.ItemsSource = Models;
this.DataContext = this;
}
public class TestModel
{
public string Key { get; set; }
public string Value { get; set; }
}
}
I bind the SelectedValue to the Model which is already existed in the List. but the selection is still blank.
What's wrong with my code? I need the combobox select the item correctly.
Model should return one of the items from Models.
Add this to the constructor before DataContext setter:
Model = Models[1];
The problematic line is
_Model = new TestModel() { Key = "Joe", Value = "456" } - you create an instance of TestModel that is not present in the list. Even though it has the same property values, it is not the same object.
ComboBoxes use the .Equals() method of the objects they are displaying to find the SelectedItem in the ItemsSource.
Since the Equals() method for clasess by default works by comparing object references, you would need to set the SelectedItem to an exact same instance as an object inside the ItemsSource.
Another approach you could take is overriding the Equals() method on your object and making it Equal by value comparison or use a record class which automatically makes your object comparable by value.

How to autogenerate DataGrid columns for collection of child type from collection of base type in WPF?

So I have a bunch of classes that are derived from some base class. I have classes (collectors) that have a methods that returns collections of these classes. I also have a TabControl where each tab has custom control that contains a DataGrid. There is a ViewModel for these custom controls. In a ViewModel I have collection of base class elements that are returned by collectors. I want to bind DataGrids to these collections and generate columns automatically, but derived classes have different properties and base class doesn't have any properties that should be shown.
internal class ElementsInfoViewModel : INotifyPropertyChanged
{
private readonly ElementScaner _elementScaner;
public ElementsInfoViewModel(ElementScaner elementScaner)
{
_elementScaner = elementScaner;
}
public ReadOnlyCollection<SystemElement> ShownElements => _elementScaner.Elements;
}
<UserControl x:Class="SysSpy.Desktop.Controls.ElementsInfoTabItemContent"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:toolkit="http://schemas.microsoft.com/wpf/2008/toolkit"
xmlns:local="clr-namespace:SysSpy.Desktop.Controls"
mc:Ignorable="d">
<Grid>
<toolkit:DataGrid ItemsSource="{Binding ShownElements}">
</toolkit:DataGrid>
</Grid>
</UserControl>
I removed everything unnecessary from code.
The idea that comes up to my mind is to cast somehow collection of base type to collection of derived type (with IValueConverter possibly), but as collection is updated many times per second, reflection might be bad solution for this
UPD
TabControl is binded to some collection in main vm
internal class MainViewModel : INotifyPropertyChanged
{
public MainViewModel()
{
Test = new ObservableCollection<ElementsInfoViewModel>();
var certificatesCollector = new CertificatesCollector();
var certificatesScaner = new ElementScaner(certificatesCollector, "Certificates");
certificatesScaner.Scan();
var certifVM = new ElementsInfoViewModel(certificatesScaner);
Test.Add(certifVM);
}
public ObservableCollection<ElementsInfoViewModel> Test { get; set;
}
}
<Window x:Class="SysSpy.Desktop.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SysSpy.Desktop"
xmlns:viewModel="clr-namespace:SysSpy.Desktop.ViewModels"
xmlns:controls="clr-namespace:SysSpy.Desktop.Controls"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<viewModel:MainViewModel x:Key="viewModelSource"/>
</Window.Resources>
<Window.DataContext>
<Binding Source="{StaticResource viewModelSource}"/>
</Window.DataContext>
<DockPanel LastChildFill="True">
<Grid>
<TabControl ItemsSource="{Binding Test}">
<TabControl.ItemTemplate>
<DataTemplate DataType="viewModel:ElementsInfoViewModel">
<controls:ElementsInfoTabItemHeader/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate DataType="viewModel:ElementsInfoViewModel">
<controls:ElementsInfoTabItemContent/>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</DockPanel>
</Window>
I don't know if this is what you need, but I have another option.
Since DataGrid doesn't have a method to dinamically bind the columns, you can extend it and make it on your own.
For example in this way:
class MyDataGrid : DataGrid
{
public MyDataGrid() : base()
{
}
protected override void OnItemsSourceChanged(System.Collections.IEnumerable oldValue, System.Collections.IEnumerable newValue)
{
base.OnItemsSourceChanged(oldValue, newValue);
if (newValue != null)
{
var enumerator = newValue.GetEnumerator();
if (enumerator.MoveNext())
{
Columns.Clear();
var firstElement = enumerator.Current;
var actualType = firstElement.GetType();
foreach (var prop in actualType.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(x => x.CanRead))
{
Columns.Add(new DataGridTextColumn
{
Header = prop.Name,
Binding = new Binding(prop.Name)
});
}
}
}
}
}
I wrote a simple project for test it and it works in my environment.
First object type (Person)
class Person
{
public string Name { get; set; }
public string Surname { get; set; }
public int Age { get; set; }
}
Second object type (Car)
class Car
{
public string Name { get; set; }
public int HP { get; set; }
}
The element you called SystemElement (Title is used as tab header)
class SystemElement
{
public SystemElement(string title, IList<object> elements)
{
Title = title;
Elements = new ReadOnlyCollection<object>(elements);
}
public string Title { get; set; }
public ReadOnlyCollection<object> Elements { get; }
}
ElementScanner that extracts the elements to show. In my test the list is set in the constructor:
class ElementScanner
{
public ElementScanner()
{
var data = new List<SystemElement>();
data.Add(new SystemElement("People", new List<object>
{
new Person { Name = "John", Surname = "Doe", Age = 22},
new Person { Name = "Lenny", Surname = "Pegasus", Age = 30},
new Person { Name = "Duffy", Surname = "Duck", Age = 22}
}));
data.Add(new SystemElement("Cars", new List<object>
{
new Car { Name = "Mercedes", HP = 700 },
new Car { Name = "Red Bull", HP = 650 },
new Car { Name = "Ferrari", HP = 600 }
}));
Elements = new ReadOnlyCollection<SystemElement>(data);
}
public ReadOnlyCollection<SystemElement> Elements { get; }
}
Now we have the ViewModel. In my example I did't use the INotifyPropertyChanged since properties will never change (it's a test project).
class ElementsInfoViewModel
{
public ElementsInfoViewModel()
{
var elementScanner = new ElementScanner();
Elements = elementScanner.Elements;
}
public ReadOnlyCollection<SystemElement> Elements { get; }
}
Now move to the view side. First of all we have the UserControl that rappresents the content of each tab. So it will show a DataGrid of type MyDataGrid:
<UserControl x:Class="Stack.ElementInfoView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Stack"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<local:MyDataGrid ItemsSource="{Binding Elements}" >
</local:MyDataGrid>
</Grid>
</UserControl>
Finally we have the main view that will merge all togather:
<Window x:Class="Stack.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Stack"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:ElementsInfoViewModel />
</Window.DataContext>
<Grid>
<TabControl ItemsSource="{Binding Elements}">
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="Header" Value="{Binding Title}" />
</Style>
</TabControl.ItemContainerStyle>
<TabControl.ContentTemplate>
<DataTemplate DataType="local:ElementsInfoViewModel">
<local:ElementInfoView></local:ElementInfoView>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Window>
This is what we have (and I suppose it is what you want):
I don't know if it works, but have you tried with Generics?
I mean something like this:
internal class ElementsInfoViewModel<T> : INotifyPropertyChanged
where T : SystemElement
{
public ElementsInfoViewModel(ElementScaner elementScaner)
{
var list = new List<T>();
foreach (var el in elementScanner.Elements)
{
list.Add(el as T);
}
ShownElements = new ReadOnlyCollection<T>(list);
}
public ReadOnlyCollection<T> ShownElements { get; }
}
I write it in the browser, so my code can be wrong, but take the idea.
If it could be nice, the best would be to have the generic version of ElementScanner (something like ElementScanner< T > ).
Let us know if it will work

WPF ListCollectionView custom grouping/sorting by List<string>

My Setup
I'm using ListCollectionView to sort and group my data in ListBox.
My model:
public class Game
{
public string Name
{
get; set;
}
public List<string> Categories
{
get; set;
}
}
What I want do to is to group by Categories, where groups in view are sorted alphabetically. And also sort all items in groups by Name.
Also I want to have group representing null or empty Categories at bottom of view.
For example:
Category1
Name2
Name3
Category2
Name2
Name3
Name4
Name5
Null/Empty
Name1
Name6
Since Categories is List<string> it means that any item can be in one or more groups or none.
Problem
I don't know how to properly implement sorting for categories. I know that I have to set multiple SortDescriptions to sort by Categories first and by Name second, but how do I do that?
WPF doesn't known how to sort List<string> groups by default so I tried to change it to custom List which implements IComparable, but that can't handle null values (comparer cannot be called if one of the objects is null).
I also tried to implement CustomSort on my view, but I thinks it's not impossible to handle my situation that way (where one item can be in multiple groups).
Update full repro code
The result is:
Null
Game1
Category2
Game2
Game3
Category1
Game3
Game4
And I want it to be:
Category1
Game3
Game4
Category2
Game2
Game3
Null
Game1
Code:
public partial class MainWindow : Window
{
public class Game
{
public string Name
{
get; set;
}
public List<string> Categories
{
get; set;
}
}
public MainWindow()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
var games = new List<Game>()
{
new Game()
{
Name = "Game1",
},
new Game()
{
Name = "Game2",
Categories = new List<string>() { "Category2" }
},
new Game()
{
Name = "Game3",
Categories = new List<string>() { "Category1", "Category2" }
},
new Game()
{
Name = "Game4",
Categories = new List<string>() { "Category1" }
}
};
var view = (ListCollectionView)CollectionViewSource.GetDefaultView(games);
view.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
view.GroupDescriptions.Add(new PropertyGroupDescription("Categories"));
MainList.ItemsSource = view;
}
}
And XAML:
<Window x:Class="grouping.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:grouping"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded">
<Grid>
<ListBox Name="MainList">
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Expander Header="{Binding Name, TargetNullValue='Null'}" IsExpanded="True">
<ItemsPresenter />
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListBox.GroupStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
Basically, your data are not in suitable format for displaying in WPF. You want the same entry to appear in multiple places (once per group), while the typical itemssource will display each item once, no matter the sort details.
So you should translate your data to a representation that is more natural to WPF. For example (with custom comparer for sorting):
public class GameListEntry : IComparable<GameListEntry>
{
public string Name { get; set; }
public string Category { get; set; }
public int CompareTo(GameListEntry other)
{
if (Category == other.Category)
{
return 0;
}
if (Category == "Null / Empty")
{
return 1;
}
if (other.Category == "Null / Empty")
{
return -1;
}
return Category.CompareTo(other.Category);
}
}
List<Game> games = ...;
var listedGameEntries = games.SelectMany(x => x.Categories.DefaultIfEmpty().Select(c => new GameListEntry { Name = x.Name, Category = string.IsNullOrEmpty(c) ? "Null / Empty" : c }));
var groupedGames = listedGameEntries.GroupBy(x => x.Category);
The CompareTo might need to be enhanced or replaced by a different mechanism in order to make the sorting and grouping work the way it should.
Edit: added DefaultIfEmpty() in order to ensure one entry even with an empty list of categories.

Binding not working properly in treeview WPF

I have an Employee Class as shown below:
public class Employee : INotifyPropertyChanged
{
public Employee()
{
_subEmployee = new ObservableCollection<Employee>();
}
public string Name { get; set; }
public ObservableCollection<Employee> SubEmployee
{
get { return _subEmployee; }
set
{
_subEmployee = value;
NotifiyPropertyChanged("SubEmployee");
}
}
ObservableCollection<Employee> _subEmployee;
public event PropertyChangedEventHandler PropertyChanged;
void NotifiyPropertyChanged(string property)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
I am creating a collection of employee class in Main window constructor
and adding it to an observable collection of employee as shown below:
public partial class MainWindow : Window
{
public ObservableCollection<Employee> Emp { get; private set; }
public MainWindow()
{
InitializeComponent();
Emp = new ObservableCollection<Employee>();
Emp.Add(new Employee(){Name = "Anuj"});
Emp.Add(new Employee() { Name = "Deepak" });
Emp.Add(new Employee() { Name = "Aarti" });
Emp[0].SubEmployee.Add(new Employee(){Name = "Tonu"});
Emp[0].SubEmployee.Add(new Employee() { Name = "Monu" });
Emp[0].SubEmployee.Add(new Employee() { Name = "Sonu" });
Emp[2].SubEmployee.Add(new Employee() { Name = "Harsh" });
Emp[2].SubEmployee.Add(new Employee() { Name = "Rahul" });
Emp[2].SubEmployee.Add(new Employee() { Name = "Sachin" });
this.DataContext = this;
}
}
I have set the DataContext as self.
Now, in xaml file I have created a hierarchical template of treeview and binded data as shown below:
<Window x:Class="WpfApplication3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<TreeView ItemsSource="{Binding}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding SubEmployee}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
</Window>
Now when I keep, TreeView ItemsSource="{Binding Emp}" , binding works properly
and I can see the tree view structure after running the code.
However when I keep TreeView ItemsSource="{Binding}", I see no result after running the code.
To my understanding, keeping ItemSource = "{Binding}" means I am binding to the evaluated value of the current datacontext.
As my datacontext is set to self, ItemSource = "{Binding}" should mean I am binding to the only property of DataContext i.e. Emp and I should get proper result.
Please help me in understanding the problem I am getting in keeping binding as
ItemSource = "{Binding}".
"To my understanding, keeping ItemSource = "{Binding}" means I am binding to the evaluated value of the current datacontext."
Correct AND that is the issue. ItemsSource expects the binding source to be of type IEnumerable but you are binding to Window.
"...should mean I am binding to the only property of DataContext i.e. Emp and I should get proper result."
No. No such "single property" assumption exists in WPFs binding conventions.
Change...
this.DataContext = this;
To...
this.DataContext = Emp;
Or, alternatively, change binding in XAML and specify the correct member on the DataContext to bind to using Path...
<TreeView ItemsSource="{Binding Path=Emp}">

changing listBox items dynamically when selection changes in comboBox

I have a comboBox which allows user to choose the selection they want. Based on the selection of the comboBox, i would display the listBox with a list of strings that is related to the user selection.
Example: User chooses "Animals" on comboBox, listBox will display "Monkeys, Horses, Pigs".
Trying to create this simple binding with minimal coding( XAML driven) but to no avail for 1 day. Thanks in advance!
Edit:
Hi for those interested in doing it another way (using only the xaml and a class to store all your data) you could check out the answer by Jehof in the link provided. It is quite a simple way to achieve this.
ListBox does not display the binding data
Here is a quick example of what you are looking for(to get you started).
First create an object that contains all your data and bind that to the ComboBox, the use the Comboboxes SelectedItem to populate the ListBox.
Code:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Categories.Add(new Category { Name = "Animals", Items = new List<string> { "Dog", "Cat", "Horse" } });
Categories.Add(new Category { Name = "Vehicles", Items = new List<string> { "Car", "Truck", "Boat" } });
}
private ObservableCollection<Category> _categories = new ObservableCollection<Category>();
public ObservableCollection<Category> Categories
{
get { return _categories; }
set { _categories = value; }
}
}
public class Category
{
public string Name { get; set; }
public List<string> Items { get; set; }
}
Xaml:
<Window x:Class="WpfApplication10.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525" Name="UI">
<StackPanel DataContext="{Binding ElementName=UI}">
<ComboBox x:Name="combo" ItemsSource="{Binding Categories}" DisplayMemberPath="Name"/>
<ListBox ItemsSource="{Binding SelectedItem.Items, ElementName=combo}"/>
</StackPanel>
</Window>
Result:

Categories