WPF ListView SelectedItem Not Updating With Expander in DataTemplate - c#

I'm creating a WPF application using C# and MVVM, and I'm having some trouble with the SelectedItem of a ListView.
In the contrived example, the scenario: I have a PersonViewModel containing a List of Person objects. Each person object has its own List of Address objects. My goal is to load all of the Person objects into a ListView. Additionally, the ListView DataTemplate should include Expanders that show the available addresses for each person.
The problem: This all actually works decently enough. The only problem is that if a user selects the expander for a person object, it doesn't actually change the SelectedItem property of the ListView (which is bound to a SelectedPerson object in the view model).
I'm relatively confident that the code works otherwise because if I click on the ListView row (outside the boundaries of the contained expander), the SelectedPerson property is updated. Is there anyway to somehow bind when the expander is clicked to the SelectedPerson property? (I'm also open to other UI ideas that will get my information across in a cleaner, more easily implemented manner).
Person.cs
using System.Collections.Generic;
namespace ListViewExpanderTest
{
public class Person
{
public Person()
{
Addresses = new List<Address>();
}
public string Name { get; set; }
public List<Address> Addresses { get; set; }
}
}
Address.cs
namespace ListViewExpanderTest
{
public class Address
{
public int HouseNumber { get; set; }
public string StreetName { get; set; }
public string City { get; set; }
}
}
PersonViewModel.cs
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace ListViewExpanderTest
{
public class PersonViewModel : INotifyPropertyChanged
{
private Person _selectedPerson;
private Address _selectedAddress;
public List<Person> People { get; set; }
public Person SelectedPerson
{
get
{
return _selectedPerson;
}
set
{
_selectedPerson = value;
OnPropertyChanged();
}
}
public Address SelectedAddress
{
get
{
return _selectedAddress;
}
set
{
_selectedAddress = value;
OnPropertyChanged();
}
}
public PersonViewModel()
{
People = new List<Person>
{
new Person
{
Name = "Person 1",
Addresses = new List<Address>
{
new Address {HouseNumber = 1, StreetName = "Fake St", City = "Fake City" },
new Address {HouseNumber = 2, StreetName = "Super Fake St", City = "Super Fake City" }
}
},
new Person
{
Name = "Person 2",
Addresses = new List<Address>
{
new Address {HouseNumber = 10, StreetName = "Fake St", City = "Fake City" },
new Address {HouseNumber = 20, StreetName = "Super Fake St", City = "Super Fake City" }
}
}
};
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
MainWindow.xaml
<Window x:Class="ListViewExpanderTest.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:ListViewExpanderTest"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:PersonViewModel />
</Window.DataContext>
<Grid>
<ListView ItemsSource="{Binding People, Mode=TwoWay}"
SelectedItem="{Binding SelectedPerson}">
<ListView.ItemTemplate>
<DataTemplate>
<Expander >
<Expander.Header>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" />
</StackPanel>
</Expander.Header>
<Expander.Content>
<ListView ItemsSource="{Binding Addresses, Mode=TwoWay}"
SelectedItem="{Binding SelectedAddress}">
<ListView.View>
<GridView>
<GridViewColumn Width="50" Header="Number" DisplayMemberBinding="{Binding HouseNumber}" />
<GridViewColumn Width="100" Header="Street" DisplayMemberBinding="{Binding StreetName}" />
<GridViewColumn Width="100" Header="City" DisplayMemberBinding="{Binding City}" />
</GridView>
</ListView.View>
</ListView>
</Expander.Content>
</Expander>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Window>

As your view model contains a single SelectedPerson property it doesn't make sense to me to include an expander in the datatemplate. If you can change your UI, the below example seems to make more sense to me. You can obviously place the address 'panel' anywhere you like.
Also, have you tried selecting an address from the sub listview? Does the binding work?
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<ListView ItemsSource="{Binding People, Mode=TwoWay}"
SelectedItem="{Binding SelectedPerson}"
DisplayMemberPath="Name">
</ListView>
<ListView Grid.Column="1"
ItemsSource="{Binding SelectedPerson.Addresses, Mode=TwoWay}"
SelectedItem="{Binding SelectedAddress}">
<ListView.View>
<GridView>
<GridViewColumn Width="50" Header="Number" DisplayMemberBinding="{Binding HouseNumber}" />
<GridViewColumn Width="100" Header="Street" DisplayMemberBinding="{Binding StreetName}" />
<GridViewColumn Width="100" Header="City" DisplayMemberBinding="{Binding City}" />
</GridView>
</ListView.View>
</ListView>
</Grid>

Related

Question about ListView binding with MVVM

I'm working on an app that has a ListView requiring a column with a checkbox. Below is an example of the XAML. IsSelected is part of the Items class, but I'm receiving the error "Cannot resolve property 'IsSelected' in data context of type 'App.ViewModel.ItemViewModel'". I do not receive any errors at runtime, but the XAML shows an error because it is looking for an IsSelected property in the ItemViewModel. If I add DataContext="{Binding Items}" I am no longer able to bind SelectedValue to SelectedItem.
Any thoughts on what I'm doing wrong or how I can restructure this in order to get it working?
<Window x:Class="App.View.AppWindow"
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:App.View"
xmlns:vm="clr-namespace:App.ViewModel"
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
xmlns:ui="http://schemas.modernwpf.com/2019"
mc:Ignorable="d"
Title="AppWindow" Height="800" Width="1100"
ResizeMode="CanMinimize>
<Grid DataContext="{StaticResource ItemViewModel}">
<ListView ItemsSource="{Binding Items}"
SelectedValue="{Binding SelectedItem}">
<ListView.View>
<GridView AllowsColumnReorder="true">
<GridViewColumn Header="Selected" Width="100">
<GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn DisplayMemberBinding="{Binding ItemNumber}"
Header="Item #"
Width="80"/>
<GridViewColumn DisplayMemberBinding="{Binding TransactionCode}"
Header="Detail"
Width="200"/>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Window>
Here is the declaration of Items in the ItemViewModel:
using App.Models;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading.Tasks;
using System.Windows;
namespace App.ViewModel;
public class ItemViewModel : INotifyPropertyChanged
{
// Property for items
private ObservableCollection<Item>? _items;
public ObservableCollection<Item>? Items
{
get => _items;
set
{
_items = value;
OnPropertyChanged(nameof(Items));
}
}
// Property for selected item
private Item? _selectedItem;
public Item? SelectedItem
{
get => _selectedItem;
set
{
_selectedItem = value;
OnPropertyChanged(nameof(SelectedItem));
}
}
// Constructor Item ViewModel
public ItemViewModel()
{
Items = new ObservableCollection<Item>();
}
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Here is the model for Item in the Model:
namespace App.Models;
public class Item
{
public bool IsSelected { get; set; }
public string? ItemNumber { get; set; }
public string? Detail { get; set; }
}

Show Object-Variables in DataGrid based on SelectedItem in other DataGrid

I created a small sample-application in WPF to demonstrate my current issue:
In my application I have two DataGrids.
The first DataGrid is bound to an ObservableCollection of my own
Person-Objects and it just shows one property of every Person-Object.
In the second DataGrid I want to show all Person-Details based on the
selected item in my first DataGrid.
Unfortunately my second DataGrid does not upate resp. show anything when I select an item in the first DataGrid and although I googled and read a lot of stuff, I'm not able to solve it myself.
So this is what I got in my DGUpdate-Application:
The MainWindow with both DataGrids, the DataContext etc.:
<Window x:Class="DGUpdate.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:DGUpdate"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainWindow_ViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<DataGrid
Grid.Column="0"
ItemsSource="{Binding MyObeservableCollectionOfPerson}"
SelectedItem="{Binding Path=SelectedPerson, Mode=TwoWay}"
AutoGenerateColumns="False"
IsReadOnly="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Last name" Width="Auto" Binding="{Binding LastName}"/>
</DataGrid.Columns>
</DataGrid>
<DataGrid
Grid.Column="1"
ItemsSource="{Binding SelectedPerson}"
AutoGenerateColumns="False"
IsReadOnly="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Last name" Width="Auto" Binding="{Binding SelectedPerson.LastName}"/>
<DataGridTextColumn Header="First name" Width="Auto" Binding="{Binding SelectedPerson.FirstName}"/>
<DataGridTextColumn Header="Age" Width="Auto" Binding="{Binding SelectedPerson.Age}"/>
<DataGridTextColumn Header="Weight" Width="Auto" Binding="{Binding SelectedPerson.Weight}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
My ViewModel that also implements INotifyPropertyChanged and where I create two demo-Persons in the Constructor:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace DGUpdate
{
public class MainWindow_ViewModel : INotifyPropertyChanged
{
#region Properties
public ObservableCollection<Person_Model> MyObeservableCollectionOfPerson { get; set; }
public Person_Model SelectedPerson { get; set; }
#endregion
#region Constructor
public MainWindow_ViewModel()
{
MyObeservableCollectionOfPerson = new ObservableCollection<Person_Model>();
MyObeservableCollectionOfPerson.Add(
new Person_Model()
{
LastName = "Doe",
FirstName = "John",
Age = 32,
Weight = 90.3
});
MyObeservableCollectionOfPerson.Add(
new Person_Model()
{
LastName = "Miller",
FirstName = "Peter",
Age = 41,
Weight = 75.7
});
}
#endregion
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}
My own Person-Class with the four Properties that I want to be displayed in the second DataGrid based on the Person I selected in my first DataGrid:
namespace DGUpdate
{
public class Person_Model
{
public string LastName { get; set; }
public string FirstName { get; set; }
public int Age { get; set; }
public double Weight { get; set; }
}
}
As you can see, in my ViewModel-Class, I created a Person-Object 'SelectedPerson' that is bound to the SelectedItem of my first DataGrid and that I use as the ItemSource of my second DataGrid.
I also played around a bit with manually calling the NotifyPropertyChanged, but this unfortunately also did not work and thus I removed it from this Sample-Application.
Can someone give me advice on how to solve the above described issue?
You must call NotifyPropertyChanged() in your property like this :
private Person_Model selectedPerson;
public Person_Model SelectedPerson {
get
{
return this.selectedPerson;
}
set
{
if (value != this.selectedPerson)
{
this.selectedPerson = value;
NotifyPropertyChanged();
}
}
}
More details on the doc:
https://learn.microsoft.com/fr-fr/dotnet/framework/winforms/how-to-implement-the-inotifypropertychanged-interface

WPF ListView/GridView Binding

I am attempting to make a simple VS 2017 Extension that is taking a object and displaying it. I have the data coming back and displaying the json in a text box, so I know the data is coming back correctly. But for some reason the gv is just showing the word "id" twice, as their are two records in the dataset. I have tried so many things I'm loosing track. Plus the documentation seems to be all over the place.
I believe there could be at least 2 issues here...
1) XAML the "Bindings"
2) Binding or adding the data to the LV?
Any help with this would be greatly appreciated!
XAML
<UserControl x:Class="DoWork.AllWorkVSControl"
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"
Background="{DynamicResource VsBrush.Window}"
Foreground="{DynamicResource VsBrush.WindowText}"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
Name="MyToolWindow">
<Grid>
<StackPanel Orientation="Vertical">
<TextBlock Margin="10" HorizontalAlignment="Center">AllWorkVS</TextBlock>
<Button Content="Click me!" Click="button1_Click" Width="120" Height="80" Name="button1"/>
<TextBox Height="200" TextWrapping="Wrap" Name="txtJson"/>
<ListView x:Name="LvIssues">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn Width="Auto" Header="Id" DisplayMemberBinding="{Binding Source='Id'}"></GridViewColumn>
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
</StackPanel>
</Grid>
</UserControl>
C#
public class People
{
public string Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
public partial class AllWorkVSControl : UserControl
{
public AllWorkVSControl()
{
InitializeComponent();
}
private void button1_Click(object sender, RoutedEventArgs e)
{
var t = Issues.GetPeopleById("2");
PopulateListView();
MessageBox.Show(t);
}
private void PopulateListView()
{
var l = GetPeople();
txtJson.Text = JsonConvert.SerializeObject(l);
foreach (var p in l)
{
LvIssues.Items.Add(p);
}
}
}
you need to set the ListView.ItemsSource.
private void PopulateListView()
{
var l = GetPeople();
txtJson.Text = JsonConvert.SerializeObject(l);
LvIssues.ItemsSource= l;
}
<ListView x:Name="LvIssues">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn Width="Auto" Header="Id" DisplayMemberBinding="{Binding Id}"></GridViewColumn>
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>

How do objects NOT implementing INPC notify changes?

Simple question for those who know the answer:
I have a basic Person class defined as follows:
public class Person
{
public Person(string name, string surname)
{
this.Name = name;
this.Surname = surname;
}
public string Name { get; set; }
public string Surname { get; set; }
}
a very simple ViewModel
public partial class SimpleViewModel : Screen
{
public SimpleViewModel()
{
this.Persons = new ObservableCollection<Person>(this.GetPersons());
}
private ObservableCollection<Person> persons;
public ObservableCollection<Person> Persons
{
get { return this.persons; }
set
{
if (this.persons == value) return;
this.persons = value;
this.NotifyOfPropertyChange(() => this.Persons);
}
}
private Person selectedPerson;
public Person SelectedPerson
{
get { return this.selectedPerson; }
set
{
if (this.selectedPerson == value) return;
this.selectedPerson = value;
this.NotifyOfPropertyChange(() => this.SelectedPerson);
}
}
IEnumerable<Person> GetPersons()
{
return
new Person[]
{
new Person("Casey", "Stoner"),
new Person("Giacomo", "Agostini"),
new Person("Troy", "Bayliss"),
new Person("Valentino", "Rossi"),
new Person("Mick", "Doohan"),
new Person("Kevin", "Schwantz")
};
}
}
and a very simple View
<Window x:Class="Test.SimpleView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="SimpleView"
Width="300"
Height="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="8"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ListView x:Name="lsv"
ItemsSource="{Binding Persons}"
SelectedItem="{Binding SelectedPerson}" Grid.RowSpan="3">
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding FirstName}" />
<GridViewColumn DisplayMemberBinding="{Binding LastName}" />
</GridView>
</ListView.View>
</ListView>
<StackPanel Grid.Row="2"
Grid.Column="1"
VerticalAlignment="Center">
<TextBox Text="{Binding SelectedPerson.FirstName, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Text="{Binding SelectedPerson.LastName, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</Grid>
</Window>
if I edit FirstName or LastName in the textbox, the listview updates.
How is that possibile if Person doesn't implement INotifyPropertyChanged?
Thanks
P.S. ViewModel inherits Screen from Caliburn.Micro
This is using the PropertyDescriptor to propagate the change notifications as described here.
I wouldn't rely on this sort of binding.
It is slower and heavier weight than implementing INPC (best
practices suggestion for POCO objects).
It only works for changes
initiated through Binding syntax. If you were to programmatically
change the value of Name, the list would not respond.
I am not sure but I think........
Your Persons property implements INotifyPropertyChanged interface and so, Indirectly all the objects or properties in your Person class also implements INotifyPropertyChanged.
I might be wrong in this case. ObservableCollection also implements INotifyPropertyChanged internally. So, there is no requirement to implement INotifyPropertyChanged in Persons Property.

How to add a new row to a datagrid(WPF toolkit) when click on a button in the outside of datagrid

I want to add a new row in the datagrid when click on a button in the outside of a datagrid.
The datagrid is bound with SQL Server database, and it have some data at runtime
I want to add new data to the database through the database
I tried a lot, but it was unsuccessful
Anyone reply me it will be very helpful for me...
Advance thanks..
If the collection of data bound to the grid implements INotifyCollectionChanged, adding a new item to the collection will add the row to the datagrid.
When you read the data from the DB, store it in an ObservableCollection (which implements this interface), then bind the data to the grid.
Example:
public class ViewModel {
public ObservableCollection<Data> Items { get; set; }
...
}
In View.xaml:
...
<DataGrid ItemsSource={Binding Path=Items}" ... />
...
And you have to set the DataContext property of the view to an instance of ViewModel.
From now on, adding/removing items from the observable collection will automatically trigger the same operation on the data grid.
XAML:
<Window x:Class="NewItemEvent.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="341" Width="567" xmlns:my="http://schemas.microsoft.com/wpf/2008/toolkit">
<Grid>
<my:DataGrid AutoGenerateColumns="False" Margin="0,0,0,29" Name="dataGrid1">
<my:DataGrid.Columns>
<my:DataGridTemplateColumn Header="Name" Width="150">
<my:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding FirstName}" Margin="3 3 3 3"/>
<TextBlock Text="{Binding LastName}" Margin="3 3 3 3"/>
</StackPanel>
</DataTemplate>
</my:DataGridTemplateColumn.CellTemplate>
<my:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding FirstName}" Margin="3 3 3 3"/>
<TextBox Text="{Binding LastName}" Margin="3 3 3 3"/>
</StackPanel>
</DataTemplate>
</my:DataGridTemplateColumn.CellEditingTemplate>
</my:DataGridTemplateColumn>
<my:DataGridTextColumn Header="Age" Binding="{Binding Age}" Width="100"/>
</my:DataGrid.Columns>
</my:DataGrid>
<Button Height="23" HorizontalAlignment="Left" Name="AddNewRow" Click="AddNewRow_Click" VerticalAlignment="Bottom" Width="75">New Row</Button>
Code:
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
ObservableCollection<Person> People = new ObservableCollection<Person>();
public Window1()
{
InitializeComponent();
dataGrid1.ItemsSource = People;
}
private void AddNewRow_Click(object sender, RoutedEventArgs e)
{
People.Add(new Person() { FirstName = "Tom", LastName = "Smith", Age = 20 });
}
}
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
}

Categories