WPF - Bind combobox to List of custom class objects - c#

I have a WPF project with a combobox that I'm trying to bind to a List of ComboboxItem objects. ComboboxItem is a class that I created for my sample project. This is partially working... I have my three items available to the combobox, but the displayed value is blank and the value of combobox.SelectedValue is null. I've seen several stackoverflow posts and other blog posts about how to do this. And as far as I can tell, I'm doing this right. But obviously I'm doing something wrong. Here is the source code for a test project...
XAML:
<Window x:Class="WpfTestApp_ComboBoxes.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"
Loaded="Window_Loaded">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<ComboBox x:Name="cboMyCombo" Grid.Row="0"
SelectionChanged="cboMyCombo_SelectionChanged"></ComboBox>
</Grid>
</Window>
C# Code-behind:
public partial class MainWindow : Window
{
List<ComboboxItem> _list = new List<ComboboxItem>();
public MainWindow()
{
_list.Add(new ComboboxItem() { DisplayValue = "One", InternalValue = "1" });
_list.Add(new ComboboxItem() { DisplayValue = "Two", InternalValue = "2" });
_list.Add(new ComboboxItem() { DisplayValue = "Three", InternalValue = "3" });
InitializeComponent();
}
private void cboMyCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count > 0)
{
ComboBox cb = sender as ComboBox;
MessageBox.Show(string.Format("Selected Item: {0}, Selected Value: {1}", cb.SelectedItem, cb.SelectedValue));
}
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
cboMyCombo.ItemsSource = _list;
cboMyCombo.DisplayMemberPath = "DisplayValue";
cboMyCombo.SelectedValuePath = "InternalValue";
}
}
ComboboxItem Class:
public class ComboboxItem
{
public string DisplayValue;
public string InternalValue;
}

change
public class ComboboxItem
{
public string DisplayValue;
public string InternalValue;
}
to
public class ComboboxItem
{
public string DisplayValue {get;set;}
public string InternalValue {get;set;}
}

Related

How to update item in UWP ListView with AdvancedCollectionView source

I am using AdvanceCollectionView from Windows Community Toolkit as a source for a XAML ListView, to allow sorting and filtering. I am having problems with updating the ListView.
To replicate the issue, I've created a simple Person class. In MainPage XAML I have a ListView MyXAMLList and a Button EditButton. In the MainPage code, I have an ObservableCollection<Person> MyPersonList and AdvancedCollectionView MyPersonACV. In Page_Loaded event I add a person to the list and use AdvancedCollectionView as a source for the list view:
Person p = new Person
{
Name = "John",
Age = 35
};
MyPersonList.Add(p);
MyPersonACV = new AdvancedCollectionView(MyPersonList, true);
MyXAMLList.ItemsSource = MyPersonACV;
This works and I can see John in the list.
In the EditButton code I try to update the item on the list but this isn't working. Both the ObservableCollection and the AdvancedCollectionView are updated, but the XAML list is still displaying the old name "John" instead of "Mary".
MyPersonList[0].Name = "Mary";
Debug.WriteLine(MyPersonList[0].ToString());
Debug.WriteLine(MyPersonACV[0].ToString());
I've tried updating the MyXAMLList.SelectedItem instead, but the same result:
Person p = (Person)MyXAMLList.SelectedItem;
p.Name = "Mary";
I've also tried adding MyPersonACV.Refresh(); but doesn't help.
What am I doing wrong? How can I update an item in the list?
Full code below
Person class:
class Person
{
public string Name {get; set;}
public int Age { get; set; }
public override string ToString()
{
return Name;
}
}
MainPage XAML:
<Page
x:Class="App3.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:App3"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
Loaded="Page_Loaded">
<Grid>
<StackPanel Orientation="Vertical">
<ListView Height="Auto" Width="Auto" x:Name="MyXAMLList" SelectionMode="Single" IsItemClickEnabled="True"/>
<StackPanel Orientation="Horizontal">
<Button x:Name="EditButton" Content="Edit" Click="EditButton_Click"/>
</StackPanel>
</StackPanel>
</Grid>
</Page>
MainPage cs:
using Microsoft.Toolkit.Uwp.UI;
using System.Collections.ObjectModel;
using System.Diagnostics;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace App3
{
public sealed partial class MainPage : Page
{
private ObservableCollection<Person> MyPersonList = new ObservableCollection<Person>();
private AdvancedCollectionView MyPersonACV;
public MainPage()
{
this.InitializeComponent();
}
private void EditButton_Click(object sender, RoutedEventArgs e)
{
//Change name
MyPersonList[0].Name = "Mary";
//Person p = (Person)MyXAMLList.SelectedItem;
//p.Name = "Mary";
Debug.WriteLine(MyPersonList[0].ToString());
Debug.WriteLine(MyPersonACV[0].ToString());
//MyPersonACV.Refresh();
}
private void Page_Loaded(object sender, RoutedEventArgs e)
{
//create person
Person p = new Person
{
Name = "John",
Age = 35
};
//add to list
MyPersonList.Add(p);
//set up ListView source
MyPersonACV = new AdvancedCollectionView(MyPersonList, true);
MyXAMLList.ItemsSource = MyPersonACV;
}
}
}
I noticed you override the ToString() method to display each item of ListView. When you update the Name property, even if the value of Name property has updated, since there is no binding relationship between Name property and ListViewItem, and the ToString() method isn't triggered when you update data, the UI isn't updated. It'sbetter to customize the appearance of items using DataTemplate, binding the Name property to the element(e.g. TetxBlock) and implement INotifyPropertyChanged interface. In this case, when the Name proeprty changes, it will provide change notifications to the binding and the UI will update. For exmaple:
.xaml:
<ListView Height="Auto" Width="Auto" x:Name="MyXAMLList" SelectionMode="Single" IsItemClickEnabled="True">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"></TextBlock>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
.cs:
public class Person : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private string name { get; set; }
public string Name
{
get
{
return name;
}
set
{
name = value;
OnPropertyChanged();
}
}
public int Age { get; set; }
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
private void EditButton_Click(object sender, RoutedEventArgs e)
{
//Change name
MyPersonList[0].Name = "Mary";
}

Binding a Stack (that can be switched) to a ListBox

I have a list of objects (Man) which each contains a Stack of states.
I have a Debug window which shows the selected Man's stack in a ListBox.
And I have a TabControl which I use to select a Man to debug.
To be able to select the correct binding, I made a property which returns the StateStack of the man at the selected index of the TabControl.
public object StateStack => Men[DebugIndex].States;
DebugIndex is bound to the TabControl's SelectedIndex property. So to make DebugIndex update the StateStack to show, I used OnPropertyChanged:
public int DebugIndex {
get => _debugIndex;
set {
_debugIndex = value;
OnPropertyChanged(nameof(StateStack));
}
}
The problem is, when the TabControl's SelectedIndex changes, the Stack is weirdly disordered! Bug the thing is that it's disordered only in the View, not really in the data.
I think it comes from something with the fact that I change the reference of the Binding it's an other Stack but I don't know how to solve that...
By the way, it works when I add all the Man objects and initialize their StateStack at the beginning. But as soon as I add a Man (and initialize its StateStack) later, for example when I click a Button, it doesn't work anymore...
public sealed partial class MainWindow : INotifyPropertyChanged {
private int _debugIndex;
public ObservableCollection<Man> Men { get; } = new ObservableCollection<Man>();
public MainWindow() {
Men.Add(new Man {Index = 0, States = new StateStack()});
InitializeComponent();
Men[0].States.Push(new State {Name = "Falling1"});
Men[0].States.Push(new State {Name = "Walking1"});
//this is simplified code. I push states here because in my program it's done during runtime (not during initialization)
}
public object StateStack => Men[DebugIndex].States;
public int DebugIndex {
get => _debugIndex;
set {
_debugIndex = value;
OnPropertyChanged(nameof(StateStack));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e) {
Men.Add(new Man {Index = 1, States = new StateStack()});
Men[1].States.Push(new State {Name = "Falling2"});
Men[1].States.Push(new State {Name = "Walking2"});
Men[1].States.Push(new State {Name = "Running2"});
}
}
public class Man {
public int Index { get; set; }
public StateStack States { get; set; }
}
public class State {
public string Name { private get; set; }
public override string ToString() {
return Name;
}
}
public sealed class StateStack : Stack<State>, INotifyCollectionChanged {
public new void Push(State item) {
base.Push(item);
OnCollectionChanged(
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, Count - 1));
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
private void OnCollectionChanged(NotifyCollectionChangedEventArgs e) {
CollectionChanged?.Invoke(this, e);
}
}
And my View code:
<Window x:Class="ObservableStackBug.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"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800" DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button Content="Add" Margin="5" Padding="8 2" HorizontalAlignment="Left" Click="ButtonBase_OnClick"/>
<ListBox ItemsSource="{Binding StateStack}" Grid.Row="1" />
<TabControl Grid.Row="2" ItemsSource="{Binding Men}" SelectedIndex="{Binding DebugIndex}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Index}" />
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
</Grid>
</Window>
What could I do to say to my binding that when DebugIndex is changed, StateStack is a very other Stack?
I've simulated your scenario and observation is that there is problem with Push method for how the NotifyCollectionChangedEventArgs is item changed is propagated to source. The current code notifies that items are changed from the end index (but for stack the items are added at Top)). If you update the notification start index to 0 as NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, 0) then bound source will display the item in appropriate order in the view. You can read about NotifyCollectionChangedEventArgs here.
public new void Push(State item) {
base.Push(item);
OnCollectionChanged(
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, 0));
}

search in DataGrid by textBox WPF

I have grid with 10-15 columns. (I load data by datagrid.ItemsSource = myList.ToList()) Also I have textBox witch textChanged event. When I put here eg. "cat" I want to see only rows with value ...cat...
how do I make this?
LINQ queries are good for this sort of thing, the concept goes make a variable to store all of your rows (in the example called _animals) and then when the user presses a key in the text box use a query, and pass the result as the ItemsSource instead.
Here is a basic working example of how this would work, first the XAML for the Window.
<Window x:Class="FilterExampleWPF.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:FilterExampleWPF"
mc:Ignorable="d"
WindowStartupLocation="CenterScreen"
Title="MainWindow" Height="350" Width="525">
<Grid>
<TextBox x:Name="textBox1" Height="22" Margin="10,10,365,0" VerticalAlignment="Top" KeyUp="textBox1_KeyUp" />
<DataGrid x:Name="dataGrid1" Height="272" Margin="10,40,10,0" VerticalAlignment="Top" AutoGenerateColumns="True" />
</Grid>
</Window>
Next the code behind:
using System.Collections.Generic;
using System.Linq;
namespace FilterExampleWPF
{
public partial class MainWindow : System.Windows.Window
{
List<Animal> _animals;
public MainWindow()
{
InitializeComponent();
_animals = new List<Animal>();
_animals.Add(new Animal { Type = "cat", Name = "Snowy" });
_animals.Add(new Animal { Type = "cat", Name = "Toto" });
_animals.Add(new Animal { Type = "dog", Name = "Oscar" });
dataGrid1.ItemsSource = _animals;
}
private void textBox1_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)
{
var filtered = _animals.Where(animal => animal.Type.StartsWith(textBox1.Text));
dataGrid1.ItemsSource = filtered;
}
}
public class Animal
{
public string Type { get; set; }
public string Name { get; set; }
}
}
For this example I created an Animal class, however you could substitute it for your own class that you need to filter. Also I enabled AutoGenerateColumns, however adding your own column bindings in WPF would still allow this to work.
Hope this helps!
This is my solution .
public class Animal
{
public string Type { get; set; }
public string Name { get; set; }
}
List<Animal> _animals = new List<Animal>();
public MainWindow()
{
InitializeComponent();
_animals.Add(new Animal { Type = "cat", Name = "Snowy" });
_animals.Add(new Animal { Type = "cat", Name = "Toto" });
_animals.Add(new Animal { Type = "dog", Name = "Oscar" });
dataGrid1.ItemsSource = _animals;
}
List<Animal> filterModeLisst = new List<Animal>();
private void searchBox_TextChanged(object sender, TextChangedEventArgs e)
{
filterModeLisst.Clear();
if (searchBox.Text.Equals(""))
{
filterModeLisst.AddRange(_animals);
} else
{
foreach (Animal anim in _animals)
{
if (anim.Name.Contains(searchBox.Text))
{
filterModeLisst.Add(anim);
}
}
}
dataGrid1.ItemsSource = filterModeLisst.ToList();
}

Column from selected Datagrid

I have code that returns me the selected row from a datagrid.
Now i want to have the value of the 3th column.
The code I have below already gives me the selected row
private void BandGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
try
{
var row_list = GetDataGridRows(BandGrid);
foreach (DataGridRow single_row in row_list)
{
if (single_row.IsSelected == true)
{
}
}
}
catch { }
}
Assuming that your DataGrid has an underlying data structure and you are not using datagridview, each row represents an object usually in a list of objects. You can just cast the selected row to the object's Type and pull the field of the cell you want. Also you don't have to loop through each one in the list. SelectedItem will have what you want.
Edited
private void BandGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Band single_row = (Band)BandGrid.SelectedItem;
string cellValue = single_row.Picture;
}
Edited End
If you have multi select feature on you may need to pull all iterating through SelectedItems. Note: don't make changes to the items in the foreach loop this will cause errors. You will need to make a copy of the data if you need to change the data.
private void dataGrid1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
listCells = new List<string>();
foreach(MyClass single_row in BandGrid.SelectedItems)
{
//do something with the object
listCells.add( single_row.Picture);
}
}
Example program. This sets the DataSource for the grid to List<MyClass> and every time the selection is changed textbox1 displays data in column c from the selected row.
public partial class MainWindow : Window
{
public class MyClass
{
public int a { get; set; }
public int b { get; set; }
public int c { get; set; }
public int d { get; set; }
}
public MainWindow()
{
InitializeComponent();
MyClass obj;
List<MyClass> bind = new List<MyClass>();
for (int i = 0; i < 10; i++)
{
obj = new MyClass();
obj.a = i;
obj.b = 2*i;
obj.c = 3*i;
obj.d = 4*i;
bind.Add(obj);
}
dataGrid1.ItemsSource = bind;
}
private void dataGrid1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
textBox1.Text = ((MyClass)dataGrid1.SelectedItem).c.ToString();
}
}
Here's the xaml
<Window x:Class="yo.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>
<DataGrid AutoGenerateColumns="True" Height="200" HorizontalAlignment="Left" Margin="116,116,0,0" Name="dataGrid1" VerticalAlignment="Top" Width="344" SelectionChanged="dataGrid1_SelectionChanged" />
<TextBox Height="23" HorizontalAlignment="Left" Margin="87,41,0,0" Name="textBox1" VerticalAlignment="Top" Width="120" />
</Grid>
protected void GridView1_SelectedIndexChanged(object sender, EventArgs e)
{
var selectedValue = GridView1.SelectedRow.Cells[2].Text;
}

SelectedItem of ContextMenu is null

I'm trying to get the SelectedItem of a ContextMenu.
XAML
<Window x:Class="WpfApplication1.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"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
<StackPanel>
<ListBox x:Name="MyListBox" ItemsSource="{Binding MyList}" SelectedItem="{Binding MySelectedItem}">
<ListBox.ContextMenu>
<ContextMenu ItemsSource="{Binding OCContext}" PreviewMouseDown="ContextMenu_PreviewMouseDown"/>
</ListBox.ContextMenu>
</ListBox>
<Button Content="Delete Item" Click="Button_Click"/>
</StackPanel>
</Grid>
</Window>
Code Behind
public partial class MainWindow : Window
{
public MainWindow()
{
OCContext = new ObservableCollection<string>();
MyList = new ObservableCollection<string>();
MyList.Add("Item 1");
MyList.Add("Item 2");
InitializeComponent();
}
public ObservableCollection<string> MyList { get; set; }
public ObservableCollection<string> OCContext { get; set; }
public string MySelectedItem { get; set; }
private void ContextMenu_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
MenuBase s = sender as MenuBase;
ItemCollection ic = s.Items;
string MyItem = "";
MyItem = (string)ic.CurrentItem;
MyList.Add(MyItem);
OCContext.Remove(MyItem);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
if (MySelectedItem != null)
{
OCContext.Add(MySelectedItem);
MyList.Remove(MySelectedItem);
}
}
}
You can Copy/Paste the code and the program should work.
The program is doing the following:
You can select an item in the ListBox. If you click on "Delete Item", the item will be deleted and added to the ContextMenu. If you click on the ContextMenu-Item, the item should be added again to the ListBox and removed from the ContextMenu. You should be able to do this over and over again...
So the ContextMenu is being binded to a collection. I get the Item with ic.CurrentItem.
The problem is that when I delete the item in the ListBox and add it again (by clicking on the item on the ContextMenu), ic.CurrentItem will be null.
Why?
Edit: Solution of Cyphryx is working, but now I'm trying to do the same by using MVVM/Binding:
XAML:
<ContextMenu x:Name="MyContext" ContextMenu="{Binding MyContextMenu}" ItemsSource="{Binding OCContext}"/>
ViewModel:
private ObservableCollection<string> _occontext;
public ObservableCollection<string> OCContext
{
get
{
if (_occontext == null)
_occontext = new ObservableCollection<string>();
MyContextMenu.Items.Clear();
foreach (var str in _occontext)
{
var item = new System.Windows.Controls.MenuItem();
item.Header = str;
item.Click += Content_MouseLeftButtonUp;
MyContextMenu.Items.Add(item);
}
return _occontext;
}
set
{
_occontext = value;
RaisePropertyChanged(() => OCContext);
}
}
private void Content_MouseLeftButtonUp(object sender, RoutedEventArgs e)
{
var s = sender as System.Windows.Controls.MenuItem;
if (s == null) return;
string ic = s.Header.ToString();
}
private System.Windows.Controls.ContextMenu _mycontextmenu;
public System.Windows.Controls.ContextMenu MyContextMenu
{
get
{
if (_mycontextmenu == null)
_mycontextmenu = new System.Windows.Controls.ContextMenu();
return _mycontextmenu;
}
set
{
_mycontextmenu = value;
RaisePropertyChanged(() => MyContextMenu);
}
}
Content_MouseLeftButtonUp is not being called?..
Rudi, from my knowledge, you cannot assign event handlers to individual objects in bound source. You can only use the WPF event handlers for the object it is tied to, hence, you'll have to fill the context menu manually, allowing you to add the event handlers at that time. In short, when you add
PreviewMouseDown="ContextMenu_PreviewMouseDown" to you WPF, the handler is assigned to the context menu, but when the binding adds the individual menu items, it does not add that handler to each item, leaving you event handless ;-) Below is code that will fix this:
WPF
<Window x:Class="WpfApplication1.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"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
<StackPanel>
<ListBox x:Name="MyListBox" ItemsSource="{Binding MyList}" SelectedItem="{Binding MySelectedItem}" Height="Auto" MinHeight="20">
<ListBox.ContextMenu>
<ContextMenu Name="ContextMenu" Opened="ContextMenu_Opened" />
</ListBox.ContextMenu>
</ListBox>
<Button Content="Delete Item" Click="Button_Click"/>
</StackPanel>
</Grid>
</Window>
Code Behind:
public MainWindow()
{
OCContext = new ObservableCollection<string>();
MyList = new ObservableCollection<string>();
MyList.Add("Item 1");
MyList.Add("Item 2");
InitializeComponent();
}
public ObservableCollection<string> MyList { get; set; }
public ObservableCollection<string> OCContext { get; set; }
public string MySelectedItem { get; set; }
private void ContextMenu_Opened(object sender, EventArgs e)
{
ContextMenu.Items.Clear();
foreach (var str in OCContext)
{
var item = new MenuItem();
item.Header = str;
item.Click += Content_MouseLeftButtonUp;
ContextMenu.Items.Add(item);
}
}
private void Content_MouseLeftButtonUp(object sender, EventArgs e)
{
var s = sender as MenuItem;
if (s == null) return;
var ic = s.Header.ToString();
MyList.Add(ic);
OCContext.Remove(ic);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
if (MySelectedItem != null)
{
OCContext.Add(MySelectedItem);
MyList.Remove(MySelectedItem);
}
}
I hope this helps.
Cyphryx

Categories