WPF Get the associated ListView in a GridView - wpf

I'm trying to implement a custom GridView for the ListView, but I need to find a way to get the ListView that is associated with the GridView, since I need to access some of the ListView properties (Width, Items, Template...).
I found an old post that was asking the same question Get the parent listview from a gridview object but it never got an answer...
If anyone has an idea, I would be glad :)
EDIT: Here some basic code from the custom GridView
public class GridViewEx : GridView
{
public ListView Owner {get; set;} // This is what I need to get
public GridViewEx()
{
}
}
EDIT2: I found another solution than the one presented by mm8. Since I also needed a custom GridViewHeaderRowPresenter, which is used in the ListView Scrollviewer Style, here is what I came up with (as for now):
public class GridViewHeaderRowPresenterEx : GridViewHeaderRowPresenter
{
private GridViewEx _GridView;
public GridViewHeaderRowPresenterEx()
{
Loaded += OnLoaded;
Unloaded += OnUnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
if (this.GetVisualParent<ListView>() is ListView lv && lv.View is GridViewEx gridView)
{
_GridView = gridView;
_GridView.Owner = lv;
}
}
private void OnUnLoaded(object sender, RoutedEventArgs e)
{
if (_GridView != null)
_GridView.Owner = null;
}
}
And here is the extension method to get the ListView from the custom GridViewHeaderRowPresenter:
public static class DependencyObjectExtensions
{
public static T GetVisualParent<T>(this DependencyObject depObj) where T : DependencyObject
{
if (VisualTreeHelper.GetParent(depObj) is DependencyObject parent)
{
var result = (parent as T) ?? GetVisualParent<T>(parent);
if (result != null)
return result;
}
return null;
}
}
The GridViewHeaderRowPresenter Loaded event is called when a GridView is added to a ListView, and the Unloaded event is called when the GridView is removed from the ListView.
I prefer this solution over the one from mm8, since it required (if I'm not mistaken) the ListView to have Items in order to work.
Thanks for the suggestions :)

You could override the PrepareItem method and use the ItemsControl.ItemsControlFromItemContainer method to get a reference to the parent ListView:
public class GridViewEx : GridView
{
public ListView Owner { get; set; }
public GridViewEx()
{
}
protected override void PrepareItem(ListViewItem item)
{
base.PrepareItem(item);
Owner = Owner ?? ItemsControl.ItemsControlFromItemContainer(item) as ListView;
}
}

You can use Binding with RelativeSource and AncestorType, try the code below:
XAML:
<Grid>
<ListView ItemsSource="{Binding}">
<ListView.View>
<GridView>
<GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
<GridViewColumn Header="Age" Width="50" DisplayMemberBinding="{Binding Age}" />
<GridViewColumn Header="Mail" Width="150" DisplayMemberBinding="{Binding Mail}" />
<GridViewColumn Header="Test">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Button
DataContext="{Binding Path=ItemsSource, RelativeSource={RelativeSource AncestorType={x:Type ListView}}}"
Content="Get ListView ItemsSource Count"
Click="Test_Click" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</Grid>
C#:
class User
{
public string Name { get; set; }
public int Age { get; set; }
public string Mail { get; set; }
}
public MainWindow()
{
InitializeComponent();
DataContext = new List<User>
{
new User() { Name = "John Doe", Age = 42, Mail = "john#doe-family.com" },
new User() { Name = "Jane Doe", Age = 39, Mail = "jane#doe-family.com" },
new User() { Name = "Sammy Doe", Age = 7, Mail = "sammy.doe#gmail.com" }
};
}
private void Test_Click(object sender, RoutedEventArgs e)
{
if (sender is FrameworkElement fe)
if (fe.DataContext is List<User> users)
MessageBox.Show($"ItemsSource items count: {users.Count()}.", "ListView Test");
}

Related

How do I pass all of the values in a gridrow in a wpf application?

I am trying to write a simple application where I display a listview using databinding containing multiple objects that lists their properties and a check box. I let the user check all of the boxes they want removed then press a button that removes the selected elements.
public partial class MainWindow : Window
{
ObservableCollection<User> Users = new ObservableCollection<User>();
public MainWindow()
{
System.Console.WriteLine("main window");
Users.Add(new User() { Name = "John Doe", Age = 42, Height = "6ft", Checked = false});
Users.Add(new User() { Name = "Jane Doe", Age = 39, Height = "6ft", Checked = false });
Users.Add(new User() { Name = "Sammy Doe", Age = 7, Height = "5ft", Checked = false });
drawFolderView();
}
private void drawFolderView()
{
InitializeComponent();
lvUsers.ItemsSource = Users;
}
private void button_Click(object sender, RoutedEventArgs e)
{
if (Users.Count > 0)
{
List<User> itemsToRemove = new List<User>();
foreach (User person in Users)
{
if (person.Checked)
{
itemsToRemove.Add(person);
}
}
foreach (User person in itemsToRemove)
{
Users.Remove(person);
}
}
else
{
System.Console.WriteLine("nothing in list");
}
drawFolderView();
}
private void CheckBox_Checked(object sender, RoutedEventArgs e)
{
}
private void CheckBox_Unchecked(object sender, RoutedEventArgs e)
{
}
}
public class User
{
public string Name { get; set; }
public int Age { get; set; }
public string Height { get; set; }
public bool Checked { get; set; }
public bool Equals(User other) {
if (Name.Equals(other.Name))
{
return true;
}
else
{
return false;
}
}
}
From reading the other questions I made the CheckBox_Checked and Unchecked methods, but I have no idea how to implement them.
Window x:Class="WpfApplication6.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:WpfApplication6"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ListView Margin="10,10,10,98" Name="lvUsers">
<ListView.View>
<GridView>
<GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
<GridViewColumn Header="Age" Width="50" DisplayMemberBinding="{Binding Age}" />
<GridViewColumn Header="Height" Width="150" DisplayMemberBinding="{Binding Height}" />
<GridViewColumn Width="60">
<GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox Margin="-4,0,-4,0" IsChecked="{Binding MyBoolProperty}" Checked="CheckBox_Checked" Unchecked="CheckBox_Unchecked" DataContext="{Binding Checked}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
<Button x:Name="button" Content="Delete" HorizontalAlignment="Left" Margin="353,243,0,0" VerticalAlignment="Top" Width="75" Click="button_Click" />
</Grid>
Is this a reasonable approach? Many of the concepts surrounding databinding in WPF still confuse me.
The Checkbox should be changed to this:
<CheckBox Margin="-4,0,-4,0" IsChecked="{Binding Checked}" />
The DataContext for the Checkbox is the User (like it is for the other columns). The IsChecked property of the Checkbox is bound to the Checked property on the User.
There is no need to call drawFolderView in the button_click method. The lvUsers.ItemsSource is already set to Users. Since Users is an ObservableCollection, it will raise events to tell WPF that items were removed/added and your listview will update itself automatically.
To work best with WPF, your User class should implement INotifyPropertyChanged. When the properties of User objects change, your bindings will get updated automatically. Right now, only the listview can change the User objects so you won't get any real benefit from INPC. But, if you added a button that looped through the Users and set User.Checked = true then you would need the INPC interface to tell WPF that the Checked property on the User objects changed so it could update the GUI.
Also, InitializeComponent() should be moved back to the top of the constructor. You only want that called when the window is being created.
Here is an updated version of your code with a few changes:
public partial class MainWindow : Window
{
ObservableCollection<User> Users = new ObservableCollection<User>();
public MainWindow()
{
InitializeComponent();
System.Console.WriteLine("main window");
Users.Add(new User() { Name = "John Doe", Age = 42, Height = "6ft", Checked = false });
Users.Add(new User() { Name = "Jane Doe", Age = 39, Height = "6ft", Checked = false });
Users.Add(new User() { Name = "Sammy Doe", Age = 7, Height = "5ft", Checked = false });
lvUsers.ItemsSource = Users;
}
private void button_Click(object sender, RoutedEventArgs e)
{
if (Users.Count > 0)
{
List<User> itemsToRemove = new List<User>();
foreach (User person in Users)
{
if (person.Checked)
{
itemsToRemove.Add(person);
}
}
foreach (User person in itemsToRemove)
{
Users.Remove(person);
}
}
else
{
System.Console.WriteLine("nothing in list");
}
}
}
public class User : System.ComponentModel.INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OPC(string propertyName) // OPC = OnPropertyChanged; a helper that raises the PropertyChanged event
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
private string _name;
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
OPC("Name");
}
}
}
private int _age;
public int Age { get { return _age; } set { if (_age != value) { _age = value; OPC("Age"); } } }
private string _height;
public string Height { get { return _height; } set { if (_height != value) { _height = value; OPC("Height"); } } }
private bool _checked;
public bool Checked { get { return _checked; } set { if (_checked != value) { _checked = value; OPC("Checked"); } } }
public bool Equals(User other)
{
if (Name.Equals(other.Name))
{
return true;
}
else
{
return false;
}
}
}

How to capture a button press in a ListView

I'm displaying a list of items in a WPF ListView, the items have a Quantity, Order Code and a Description. The columns are bound to fields in an ObservableCollection held in the View Model. This is all very standard and works as would expect. However, in the Quantity Column of the ListView I am adding two button + and -, the idea being that when they are pressed the value of the quantity either increments or decrements. The problem is that because these buttons are not bound to a field in the ObservableCollection I cannot get a link from the button being pressed in the List View to the record in the ObservableCollection. I have tried getting the item selected in the ListView but it is the button that gets selected when pressed and not the ListView item, I have also captured the item beneath the mouse pointer when the button is pressed but it could be pressed using the keyboard.
I feel there must be a (simple!) way of doing this but I can't find it.
This is the XAML:
<ListViewName="AccessoriesContent" >
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn Header="Select">
<GridViewColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Name="QuantityStack">
<Button Name="SubtractAccessoryButton" Command="vx:DataCommands.SubtractAccessory" Content="-" />
<TextBox Name="QuantityTextBox" Text="{Binding Quantity, Mode=TwoWay}" />
<Button Name="AddAccessoryButton" Command="vx:DataCommands.AddAccessory" Content="+" />
</StackPanel>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Order Code" DisplayMemberBinding="{Binding OrderCode}" />
<GridViewColumn Header="Description" DisplayMemberBinding="{Binding Description}" />
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
The code behind:
public MainWindow()
{
//CommandBindings.Add(
InitializeComponent();
AccessoryVM = new AccessoryViewModel();
AccessoriesContent.ItemsSource = AccessoryVM.AccessoryCollection;
}
And the ViewModel:
class AccessoryViewModel
{
ObservableCollection<AccessoryData> _AccessoryCollection =
new ObservableCollection<AccessoryData>();
public ObservableCollection<AccessoryData> AccessoryCollection
{ get { return _AccessoryCollection; } }
public void PopulateAccessories(string order_code)
{
// Read the data and populate AccessoryCollection
}
}
public class AccessoryData : INotifyPropertyChanged
{
private int _quantity;
public int Quantity
{
get { return _quantity; }
set
{
this._quantity = value;
Notify("Quantity");
}
}
public string OrderCode { get; set; }
public string Description { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected void Notify(string propName)
{
if (this.PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
}
Beyond this I have two methods SubtractAccessory and AddAccessory which are triggered by the buttons but I have yet to populate them with anything that would work.
Another option is to create a RelayCommand (see here). In this model you create an ICommand property on each of your items. You then set this property to a new RelayCommand that accepts a delegate you would like to be ran when that command is activated. So this could be a QuantityUp method and a QuantityDown method on your AccessoryData. Once you've got your ICommand property in place you simply bind to it like this, where QuantityUpCommand is your ICommand property.
<GridViewColumn Header="" >
<GridViewColumn.CellTemplate>
<DataTemplate>
<Button Height="15" Width="15" Content="+" Command="{Binding QuantityUpCommand}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
The AccessoryData would look something like this
private RelayCommand _quantityUpCommand;
public ICommand QuantityUpCommand
{
get
{
if (_quantityUpCommand == null)
{
_quantityUpCommand = new RelayCommand(QuantityUp);
}
return _quantityUpCommand;
}
}
public void QuantityUp(object obj)
{
Quantity++;
}
And RelayCommand looks something like this:
public class RelayCommand: ICommand
{
#region Fields
readonly Action<object> _execute;
readonly Predicate<object> _canExecute;
#endregion // Fields
#region Constructors
public RelayCommand(Action<object> execute)
: this(execute, null)
{
}
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute;
}
#endregion // Constructors
#region ICommand Members
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter)
{
_execute(parameter);
}
#endregion // ICommand Members
}
You do not appear to have posted the commands you use.
Anyway, if you do use commands you could either use instance commands which exist on the view model (you then will need to bind the command to the command property on the DataContext) and hence have access to the Quantity or you can pass the view model as CommandParameter just setting it to {Binding}, then in the command you can cast the parameter to the VM and change the Quantity.
(If you were to use the Click event you could just cast the sender to Button and cast its DataContext to the VM)
You could pass the current item through a CommandParameter on the button that uniquely identifies the current item. So that in the execution of the Command you know what item you're talking about. If you can't find a unique token in your item, you could even pass the whole item!
<Button Name="AddAccessoryButton" Command="vx:DataCommands.AddAccessory" CommandParameter="{Binding}" Content="+" />

WPF ListView setting SelectedItem

I've tried to search for an answer to this but I'm not having any luck. Basically I have a listview that is bound to a collection returned from a view model. I bind the selected item of the list view to a property in my listview in order to perform validation to ensure that an item is selected. The problem is that sometimes I want to load this listview with one of the items already selected. I was hoping to be able to set the property on my view model with the object I want selected and have it automatically select that item. This is not happening. My listview loads without an item selected. I can successfully set the selected index to the 0th index so why shouldn't I be able to set the selected value. The list view is in single selection mode.
Here's the pertinent code from my list view
<ListView Name="listView1" ItemsSource="{Binding Path=AvailableStyles}" SelectionMode="Single">
<ListView.SelectedItem>
<Binding Path="SelectedStyle" ValidatesOnDataErrors="True" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" BindingGroupName="StyleBinding" >
</Binding>
</ListView.SelectedItem>
<ListView.View>
<GridView>
<GridViewColumn Header="StyleImage">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Image Source="800.jpg"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Style Code" DisplayMemberBinding="{Binding StyleCode}"/>
<GridViewColumn Header="Style Name" DisplayMemberBinding="{Binding StyleName}"/>
</GridView>
</ListView.View>
</ListView>
And here is the pertinent code from my view model
public class StyleChooserController : BaseController, IDataErrorInfo, INotifyPropertyChanged
{
private IList<Style> availableStyles;
private Style selectedStyle;
public IList<Style> AvailableStyles
{
get { return availableStyles; }
set
{
if (value == availableStyles)
return;
availableStyles = value;
OnPropertyChanged("AvailableStyles");
}
}
public Style SelectedStyle
{
get { return selectedStyle; }
set
{
//if (value == selectedStyle)
// return;
selectedStyle = value;
OnPropertyChanged("SelectedStyle");
}
}
public StyleChooserController()
{
AvailableStyles = StyleService.GetStyleByVenue(1);
if (ApplicationContext.CurrentStyle != null)
{
SelectedStyle = ApplicationContext.CurrentStyle;
}
}
public string Error
{
get { return null; }
}
public string this[string columnName]
{
get
{
string error = string.Empty;
if (columnName == "SelectedStyle")
{
if (SelectedStyle == null)
{
error = "required";
}
}
return error;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
}
I should note that the "Style" referenced here has nothign to do with WPF. It's a business object. I'm really looking for a solution that doesn't break the MVVM pattern, but I'd be willing to just get something functioning. I've attempted to loop through the Listview.Items list just to set it manually but it's always empty when I try. Any help is appreciated.
Edit: I updated the code to use INotifyPropertyChanged. It's still not working. Any other suggestions
2nd Edit: I added UpdateSourceTrigger="PropertyChanged". That still did not work.
Thanks
Your problem is most likely caused because your SelectedItem Style is a different Style instance than the matching one in the AvailableStyles in the ItemsSource.
What you need to do is provide your specific definition of equality in your Style class:
public class Style: IEquatable<Style>
{
public string StyleCode { get; set; }
public string StyleName { get; set; }
public virtual bool Equals(Style other)
{
return this.StyleCode == other.StyleCode;
}
public override bool Equals(object obj)
{
return Equals(obj as Style);
}
}
Hmm... it looks like you forgot to implement INotifyPropertyChanged for the SelectedStyle property...

Silverlight Relating Two Datagrids

My question: How do I bind the SelectedItem from a primary datagrid to the ItemsSource for a secondary datagrid?
In detail:
I have two datagrids on my view. The first shows a collection of teams and the second shows as list of people in the selected team.
When I select a team from the grid I can see that the SelectedTeam property is getting updated correctly, but the People grid is not getting populated.
Note: I am not able to use nested grids, or the cool master-detail features provided in the SL data-grid.
UPDATE: Replacing the parent datagrid with a ComboBox gives completely different results and works perfectly. Why would ComboBox.SelectedItem and DataGrid.SelectedItem behave so differently?
Thanks,
Mark
Simple Repro:
VIEW:
<UserControl x:Class="NestedDataGrid.MainPage"
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"
xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data">
<StackPanel x:Name="LayoutRoot">
<TextBlock Text="Teams:" />
<data:DataGrid ItemsSource="{Binding Teams}"
SelectedItem="{Binding SelectedTeam, Mode=TwoWay}"
AutoGenerateColumns="False">
<data:DataGrid.Columns>
<data:DataGridTextColumn Header="Id" Binding="{Binding TeamId}" />
<data:DataGridTextColumn Header="Desc" Binding="{Binding TeamDesc}" />
</data:DataGrid.Columns>
</data:DataGrid>
<TextBlock Text="Peeps:" />
<data:DataGrid ItemsSource="{Binding SelectedTeam.People}"
AutoGenerateColumns="False">
<data:DataGrid.Columns>
<data:DataGridTextColumn Header="Id"
Binding="{Binding PersonId}" />
<data:DataGridTextColumn Header="Name"
Binding="{Binding Name}" />
</data:DataGrid.Columns>
</data:DataGrid>
</StackPanel>
</UserControl>
CODE_BEHIND:
using System.Windows.Controls;
namespace NestedDataGrid
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
this.LayoutRoot.DataContext = new ViewModel();
}
}
}
VIEWMODEL:
using System.Collections.ObjectModel;
namespace NestedDataGrid
{
public class ViewModel: ObjectBase
{
public ViewModel()
{
ObservableCollection<Person> RainbowPeeps = new ObservableCollection<Person>()
{
new Person(){ PersonId=1, Name="George"},
new Person(){ PersonId=2, Name="Zippy"},
new Person(){ PersonId=3, Name="Bungle"},
};
ObservableCollection<Person> Simpsons = new ObservableCollection<Person>()
{
new Person(){ PersonId=4, Name="Moe"},
new Person(){ PersonId=5, Name="Barney"},
new Person(){ PersonId=6, Name="Selma"},
};
ObservableCollection<Person> FamilyGuyKids = new ObservableCollection<Person>()
{
new Person(){ PersonId=7, Name="Stewie"},
new Person(){ PersonId=8, Name="Meg"},
new Person(){ PersonId=9, Name="Chris"},
};
Teams = new ObservableCollection<Team>()
{
new Team(){ TeamId=1, TeamDesc="Rainbow", People=RainbowPeeps},
new Team(){ TeamId=2, TeamDesc="Simpsons", People=Simpsons},
new Team(){ TeamId=3, TeamDesc="Family Guys", People=FamilyGuyKids },
};
}
private ObservableCollection<Team> _teams;
public ObservableCollection<Team> Teams
{
get { return _teams; }
set
{
SetValue(ref _teams, value, "Teams");
}
}
private Team _selectedTeam;
public Team SelectedTeam
{
get { return _selectedTeam; }
set
{
SetValue(ref _selectedTeam, value, "SelectedTeam");
}
}
}
}
ASSOCIATED CLASSES:
using System;
using System.ComponentModel;
namespace NestedDataGrid
{
public abstract class ObjectBase : Object, INotifyPropertyChanged
{
public ObjectBase()
{ }
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void _OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler pceh = PropertyChanged;
if (pceh != null)
{
pceh(this, new PropertyChangedEventArgs(propertyName));
}
}
protected virtual bool SetValue<T>(ref T target, T value, string propertyName)
{
if (Object.Equals(target, value))
{
return false;
}
target = value;
_OnPropertyChanged(propertyName);
return true;
}
}
public class Person: ObjectBase
{
private int _personId;
public int PersonId
{
get { return _personId; }
set
{
SetValue(ref _personId, value, "PersonId");
}
}
private string _name;
public string Name
{
get { return _name; }
set
{
SetValue(ref _name, value, "Name");
}
}
}
public class Team : ObjectBase
{
private int _teamId;
public int TeamId
{
get { return _teamId; }
set
{
SetValue(ref _teamId, value, "TeamId");
}
}
private string _teamDesc;
public string TeamDesc
{
get { return _teamDesc; }
set
{
SetValue(ref _teamDesc, value, "TeamDesc");
}
}
private ObservableCollection<Person> _people;
public ObservableCollection<Person> People
{
get { return _people; }
set
{
SetValue(ref _people, value, "People");
}
}
}
}
UPDATE
Replacing the first datagrid with a combobox and eveything works OK. Why would DataGrid.SelectedItem and ComboBox.SelectedItem behave so differently?
<StackPanel x:Name="LayoutRoot">
<TextBlock Text="Teams:" />
<ComboBox SelectedItem="{Binding SelectedTeam, Mode=TwoWay}"
ItemsSource="{Binding Teams}"/>
<TextBlock Text="{Binding SelectedTeam}" />
<TextBlock Text="Peeps:" />
<data:DataGrid ItemsSource="{Binding SelectedTeam.People}" />
</StackPanel>
Having done some tests.
First I just wanted to confirm that the Binding itself is working. It works quite happly when the second DataGrid is swapped out for a ListBox. I've gone so far to confirm that the second DataGrid is having its ItemsSource property changed by the binding engine.
I've also swapped out the first DataGrid for a ListBox and then the second DataGrid starts working quite happly.
In addition if you wire up the SelectionChanged event on the first datagrid and use code to assign directly to the second datagrid it starts working.
I've also removed the SelectedItem binding on the first Grid and set up an ElementToElement bind to it from the on the ItemsSource property of the second Grid. Still no joy.
Hence the problem is narrowed down to SelectedItem on one DatGrid to the ItemsSource of another via the framework binding engine.
Reflector provides a possible clue. The Data namespace contains an Extensions static class targeting DependencyObject which has an AreHandlersSuspended method backed bye a static variable. The which the code handling changes to the ItemsSource property uses this method and does nothing if it returns true.
My unconfirmed suspicion is that in the process of the first Grid assigning its SelectedItem property it has turned on the flag in order to avoid an infinite loop. However since this flag is effectively global any other legitmate code running as a result of this SelectedItem assignment is not being executed.
Anyone got SL4 and fancy testing on that?
Any MSFTers lurking want to look into?
If SL4 still has it this will need reporting to Connect as a bug.
A better solution is to use add DataGridRowSelected command. This fits the MVVM pattern a whole lot better than my previous mouse click example.
This was inspired by some code from John Papa, I have created a detailed post about this http://thoughtjelly.blogspot.com/2009/12/binding-selecteditem-to-itemssource.html.
[Sits back contented and lights a cigar]
Mark
I had the same problem, and "fixed" it by adding this to my code-behind.
Code behind:
private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_model != null)
{
_model.RefreshDetail();
}
}
Model:
public void RefreshDetail()
{
RaisePropertyChanged("Detail");
}
I have a work-around. It involves a bit of code behind, so won't be favoured by purist MVVM zealots! ;-)
<StackPanel x:Name="LayoutRoot">
<TextBlock Text="Teams:" />
<data:DataGrid x:Name="dgTeams"
SelectedItem="{Binding SelectedTeam, Mode=TwoWay}"
ItemsSource="{Binding Teams}" />
<TextBlock Text="{Binding SelectedTeam}" />
<TextBlock Text="Peeps:" />
<data:DataGrid x:Name="dgPeeps" />
</StackPanel>
Code Behind:
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
this.LayoutRoot.DataContext = new ViewModel();
dgTeams.MouseLeftButtonUp += new MouseButtonEventHandler(dgTeams_MouseLeftButtonUp)
}
void dgTeams_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
DataGridRow row = DependencyObjectHelper.FindParentOfType<DataGridRow>(e.OriginalSource as DependencyObject);
///get the data object of the row
if (row != null && row.DataContext is Team)
{
dgPeeps.ItemsSource = (row.DataContext as Team).People;
}
}
}
The FindParentOfType method is detailed here: http://thoughtjelly.blogspot.com/2009/09/walking-xaml-visualtree-to-find-parent.html.
Hope this helps someone else.

C#/WPF: ListView not updating (but when I check with Snoop, everything looks fine)

Does anyone know why my ListView with following Code is not working?
I checked it out with Snoop and the ItemsSource seems to be fine (and when I start Snoop, the ListView displays me the MyViewModel.MyCollection, but when debugging with Visual Studio it shows me nothing?)
Thank you!
PS: MainWindow.xaml.cs has the DataContext = MainViewModel
<ListView Grid.Row="1" Margin="38,50,0,168" HorizontalAlignment="Left" Name="listViewSelectDate" Width="105"
ItemsSource="{Binding Path=MyViewModel.MyCollection}"
SelectedItem="{Binding SelectedDate}" IsSynchronizedWithCurrentItem="True">
<ListView.View>
<GridView>
<GridViewColumn Header="Date" DisplayMemberBinding="{Binding Path=CalcDate}"/>
</GridView>
</ListView.View>
</ListView>
The ViewModel looks like this:
class MainViewModel : ViewModelBase
{
public SummaryViewModel MyViewModel
{
get { return _myViewModel; }
set { _myViewModel = value; RaisePropertyChanged("MyViewModel"); }
}
public MyDate SelectedDate
{
get { return _selectedDate; }
set { _selectedDate = value; RaisePropertyChanged("SelectedDate"); }
}
}
and
public class SummaryViewModel : ViewModelBase
{
public ObservableCollection<MyDate> MyCollection { get; set; }
}
and
public class MyDate
{
public DateTime CalcDate { get; set; }
}
Who sets MyCollection? It is not providing change notification, so the binding doesn't know that it has been changed. Change to:
private ObservableCollection<MyDate> _myCollection;
public ObservableCollection<MyDate> MyCollection
{
get { return _myCollection; }
set
{
_myCollection = value;
OnPropertyChanged("MyCollection");
}
}
Or, even better, make it read-only:
private readonly ICollection<MyDate> _myCollection = new ObservableCollection<MyDate>();
public ICollection<MyDate> MyCollection
{
get { return _myCollection; }
}
Look in the Visual Studio Output window, it will show any DataBinding errors that you may be getting which may help you resolve the issue.

Resources