I'm binding an observsable collection of model objects to a data grid. But when I set the binding to the collection, I get a path error to the peoprties.
In debugging this issue, I've checked that the public properties in the CustomerModel are correctly named in the DataGrid binding. And also that the collection being returned to the model isn't empty. I also checked that the data context is set correctly in the View's code behind.
I think it might be an error due to the way I've specified the binding path in the xaml..
The full details of the binding error is as follows, for each field:
System.Windows.Data Error: 40 : BindingExpression path error: 'FirstName' property not found on 'object' ''MainViewModel' (HashCode=55615518)'. BindingExpression:Path=FirstName; DataItem='MainViewModel' (HashCode=55615518); target element is 'TextBox' (Name='fNameTbx'); target property is 'Text' (type 'String')
System.Windows.Data Error: 40 : BindingExpression path error: 'LastName' property not found on 'object' ''MainViewModel' (HashCode=55615518)'. BindingExpression:Path=LastName; DataItem='MainViewModel' (HashCode=55615518); target element is 'TextBox' (Name='lNameTbx'); target property is 'Text' (type 'String')
System.Windows.Data Error: 40 : BindingExpression path error: 'Email' property not found on 'object' ''MainViewModel' (HashCode=55615518)'. BindingExpression:Path=Email; DataItem='MainViewModel' (HashCode=55615518); target element is 'TextBox' (Name='emailTbx'); target property is 'Text' (type 'String')
Could anyone point me in the right direction, in order to debug this further?
DataGrid binding path and source are set as follows:
<DataGrid Name="infogrid"
Grid.Row="0"
Grid.RowSpan="3"
Grid.Column="1"
Grid.ColumnSpan="3"
AutoGenerateColumns="False"
ItemsSource="{Binding Customers}"
SelectedItem="{Binding SelectedCustomer}">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Customers.Id}" Header="ID" />
<DataGridTextColumn Binding="{Binding Customers.FirstName}" Header="First Name" />
<DataGridTextColumn Binding="{Binding Customers.LastName}" Header="Last Name" />
<DataGridTextColumn Binding="{Binding Customers.Email}" Header="Email" />
</DataGrid.Columns>
</DataGrid>
The View Model contains an Observable collection of type CustomerModel, called Customers. This is what I've set the DataGrid ItemSource to. (I've removed other code from VM for readability)
namespace MongoDBApp.ViewModels
{
class MainViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private ICustomerDataService _customerDataService;
public MainViewModel(ICustomerDataService customerDataService)
{
this._customerDataService = customerDataService;
QueryDataFromPersistence();
}
private ObservableCollection<CustomerModel> customers;
public ObservableCollection<CustomerModel> Customers
{
get
{
return customers;
}
set
{
customers = value;
RaisePropertyChanged("Customers");
}
}
private void QueryDataFromPersistence()
{
Customers = _customerDataService.GetAllCustomers().ToObservableCollection();
}
private void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
And these are the fields that in the CustomerModel, so not sure why the properties are not being found during binding:
public class CustomerModel : INotifyPropertyChanged
{
private ObjectId id;
private string firstName;
private string lastName;
private string email;
[BsonElement]
ObservableCollection<CustomerModel> customers { get; set; }
/// <summary>
/// This attribute is used to map the Id property to the ObjectId in the collection
/// </summary>
[BsonId]
public ObjectId Id { get; set; }
[BsonElement("firstName")]
public string FirstName
{
get
{
return firstName;
}
set
{
firstName = value;
RaisePropertyChanged("FirstName");
}
}
[BsonElement("lastName")]
public string LastName
{
get
{
return lastName;
}
set
{
lastName = value;
RaisePropertyChanged("LastName");
}
}
[BsonElement("email")]
public string Email
{
get
{
return email;
}
set
{
email = value;
RaisePropertyChanged("Email");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
This is how the data context is set in the View's code behind:
public partial class MainView : Window
{
private MainViewModel ViewModel { get; set; }
private static ICustomerDataService customerDataService = new CustomerDataService(CustomerRepository.Instance);
public MainView()
{
InitializeComponent();
ViewModel = new MainViewModel(customerDataService);
this.DataContext = ViewModel;
}
}
These binding errors are not related to your DataGrid.
They indicate that you have 3 TextBoxes somewhere of the names fNameTbx, lNameTbx, and emailTbx. A DataGrid does not generate it's items with a Name property, so it is not the one causing these binding errors.
When trying to read binding errors, it's best to break them up by semi-colons and read them backwards, as demonstrated here.
For example,
System.Windows.Data Error: 40 : BindingExpression path error: 'FirstName' property not found on 'object' ''MainViewModel' (HashCode=55615518)'. BindingExpression:Path=FirstName; DataItem='MainViewModel' (HashCode=55615518); target element is 'TextBox' (Name='fNameTbx'); target property is 'Text' (type 'String')
Can also be read as
target property is 'Text' (type 'String')
target element is 'TextBox' (Name='fNameTbx');
DataItem='MainViewModel' (HashCode=55615518);
BindingExpression path error: 'FirstName' property not found on 'object' ''MainViewModel' (HashCode=55615518)'. BindingExpression:Path=FirstName;
Meaning somewhere you have
<TextBox Name="fNameTbx" Text="{Binding FirstName}" />
Where the DataContext of this TextBox is of type MainViewModel. And MainViewModel does not have a property of FirstName.
I'd recommend searching your project for those names, or you could use a tool like Snoop to debug databindings and DataContext issues at runtime.
The exceptions indicate that the DataBinding engine is looking for the fields FirstName, LastName, etc. on MainViewModel as opposed to CustomerModel.
You don't need to specify the property Customers in the individual binding expressions for the columns:
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Id}" Header="ID" />
<DataGridTextColumn Binding="{Binding FirstName}" Header="First Name" />
<DataGridTextColumn Binding="{Binding LastName}" Header="Last Name" />
<DataGridTextColumn Binding="{Binding Email}" Header="Email" />
</DataGrid.Columns>
I was having the same issue when I had the TextBlock Text Binding inside of a DataTemplate and I ended up having to do:
Text={Binding DataContext.SuccessTxt}
to get it to work properly. Try adding "DataContext." in front of the property and see if that works.
public Window()
{
this.DataContext = this;
InitializeComponent();
}
public string Name {get;set;}
//xaml
<TextBlock Text="{Binding Name}"/>
this.DataContext = this; before InitializeComponent();
(DataContext need available before load xaml in InitializeComponent())
Properties Name should be public and { get; }
(If private then wpf can't access)
Related
I have an ItemsControl that should display the values of some properties of an object.
The ItemsSource of the ItemsControl is an object with two properties: Instance and PropertyName.
What I am trying to do is displaying all the property values of the Instance object, but I do not find a way to set the Path of the binding to the PropertyName value:
<ItemsControl ItemsSource={Binding Path=InstanceProperties}>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=PropertyName, Mode=OneWay}"/>
<TextBlock Text=": "/>
<TextBlock Text="{Binding Source=??{Binding Path=Instance}??, Path=??PropertyName??, Mode=OneWay}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
the question marks are the points where I don't know how to create the binding.
I initially tried with a MultiValueConverter:
<TextBlock Grid.Column="1" Text="{Binding}">
<TextBlock.DataContext>
<MultiBinding Converter="{StaticResource getPropertyValue}">
<Binding Path="Instance" Mode="OneWay"/>
<Binding Path="PropertyName" Mode="OneWay"/>
</MultiBinding>
</TextBlock.DataContext>
</TextBlock>
The MultiValueConverter uses Reflection to look through the Instance and returns the value of the property.
But if the property value changes, this change is not notified and the displayed value remains unchanged.
I am looking for a way to do it with XAML only, if possible, if not I will have to write a wrapper class to for the items of the ItemsSource collection, and I know how to do it, but, since it will be a recurring task in my project, it will be quite expensive.
Edit:
For those who asked, InstanceProperties is a property on the ViewModel which exposes a collection of objects like this:
public class InstanceProperty : INotifyPropertyChanged
{
//[.... INotifyPropertyChanged implementation ....]
public INotifyPropertyChanged Instance { get; set; }
public string PropertyName { get; set; }
}
Obviously the two properties notify theirs value is changing through INotifyPropertyChanged, I don't include the OnPropertyChanged event handling for simplicity.
The collection is populated with a limited set of properties which I must present to the user, and I can't use a PropertyGrid because I need to filter the properties that I have to show, and these properties must be presented in a graphically richer way.
Thanks
Ok, thanks to #GazTheDestroyer comment:
#GazTheDestroyer wrote: I cannot think of any way to dynamically iterate and bind to an arbitrary object's properties in XAML only. You need to write a VM or behaviour to do this so you can watch for change notifications, but do it in a generic way using reflection you can just reuse it throughout your project
I found a solution: editing the ViewModel class InstanceProperty like this
added a PropertyValue property
listen to PropertyChanged event on Instance and when the PropertyName value changed is fired, raise PropertyChanged on PropertyValue
When Instance or PropertyName changes, save a reference to Reflection's PropertyInfo that will be used by PropertyValue to read the value
here is the new, complete, ViewModel class:
public class InstanceProperty : INotifyPropertyChanged
{
#region Properties and events
public event PropertyChangedEventHandler PropertyChanged;
private INotifyPropertyChanged FInstance = null;
public INotifyPropertyChanged Instance
{
get { return this.FInstance; }
set
{
if (this.FInstance != null) this.FInstance.PropertyChanged -= Instance_PropertyChanged;
this.FInstance = value;
if (this.FInstance != null) this.FInstance.PropertyChanged += Instance_PropertyChanged;
this.CheckProperty();
}
}
private string FPropertyName = null;
public string PropertyName
{
get { return this.FPropertyName; }
set
{
this.FPropertyName = value;
this.CheckProperty();
}
}
private System.Reflection.PropertyInfo Property = null;
public object PropertyValue
{
get { return this.Property?.GetValue(this.Instance, null); }
}
#endregion
#region Private methods
private void CheckProperty()
{
if (this.Instance == null || string.IsNullOrEmpty(this.PropertyName))
{
this.Property = null;
}
else
{
this.Property = this.Instance.GetType().GetProperty(this.PropertyName);
}
this.RaisePropertyChanged(nameof(PropertyValue));
}
private void Instance_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == this.PropertyName)
{
this.RaisePropertyChanged(nameof(PropertyValue));
}
}
private void RaisePropertyChanged(string propertyname)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyname));
}
#endregion
}
and here is the XAML:
<ItemsControl ItemsSource={Binding Path=InstanceProperties}>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=PropertyName, Mode=OneWay}"/>
<TextBlock Text=": "/>
<TextBlock Text="{Binding Path=PropertyValue, Mode=OneWay}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Please help me to figure out how to work with ComboBoxColumn in WPF's DataGrid.
I'm trying to create a list of devices, where each device have dynamic list of states in field "log".
<DataGrid AutoGenerateColumns="False" Margin="12,6,12,12" Name="dataGrid1" Grid.Row="1" SelectionUnit="FullRow">
<DataGrid.Columns>
...
<DataGridComboBoxColumn Header="Log"
ItemsSource="{Binding log, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Device}}}"/>
</DataGrid.Columns>
</DataGrid>
public partial class MainWindow : Window
{
public ObservableCollection<Device> devices;
...
}
public MainWindow()
{
...
dataGrid1.ItemSource = devices;
}
public class Device : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public Device() {log = new ObservableCollection<string>();}
...
private ObservableCollection<string> _log;
public ObservableCollection<string> log { get { return _log; }
set { _log = value; OnPropertyChanged("log"); } }
}
Can you share any suggestions: How can i show in each combobox in datagrid list "log" of each object?
MSDN: DataGridComboboxColumns says:
To populate the drop-down list, first set the ItemsSource property for
the ComboBox by using one of the following options:
A static resource. For more information, see StaticResource Markup Extension.
An x:Static code entity. For more information, see x:Static Markup Extension.
An inline collection of ComboBoxItem types.
So basically to just bind to data object`s collection property it`s better to use DataGridTemplateColumn:
<DataGridTemplateColumn Header="Log">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding log}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
This type of column gives you some more posibilities for templating too.
I have a WPF datagrid with a combobox column and two textbox columns. In my test case, when the screen is loaded, there are two rows in the collection to which the grid is bound. If I change the contents of any of the cells, it updates properly. However, if I add a new row to the grid, when I update the value in the combobox column, it is not updated in the source collection. The textbox columns work properly for newly added rows though. The columns are defined as such:
<DataGrid.Columns>
<DataGridComboBoxColumn Header="Type" Width="*" SelectedValueBinding="{Binding Mode=TwoWay, Path=Type.Id}"
ItemsSource="{Binding Source={StaticResource PhoneTypeList}, Path=PhoneTypes}"
SelectedValuePath="Id" DisplayMemberPath="Type" />
<DataGridTextColumn Binding="{Binding NotifyOnTargetUpdated=True, Path=Number, Mode=TwoWay, ValidatesOnExceptions=False}" Header="Number" Width="*"/>
<DataGridTextColumn Binding="{Binding NotifyOnSourceUpdated=True, Path=Extension, ValidatesOnExceptions=False}" Header="Extension" Width="*"/>
</DataGrid.Columns>
Here is the PhoneNumbers property in my viewmodel:
public ObservableCollection<PhoneNumber> PhoneNumbers
{
get
{
return _phoneNumbers;
}
set
{
if (value != _phoneNumbers)
{
_phoneNumbers = value;
OnPropertyChanged("PhoneNumbers");
}
}
}
Update: Here is my PhoneNumber class:
public class PhoneNumber : INotifyPropertyChanged
{
private string _number;
private string _extension;
private PhoneType _type;
public PhoneType Type { get { return _type; }
set { _type = value; OnPropertyChanged("Type"); } }
public string Number
{
set
{
_number = value;
OnPropertyChanged("Number");
}
get { return _number; }
}
public string Extension
{
set
{
_extension = value;
OnPropertyChanged("Extension");
}
get { return _extension; }
}
public override string ToString()
{
return Number + (!string.IsNullOrEmpty(Extension) ? " x " + Extension : "");
}
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this,
new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
}
First, check your output window for any binding error messages.
It looks to me like you Type property might be null when you add a new item. This essentially makes the Id property of Type inaccessible.
Try instantiating a new default Type object in your PhoneNumber Constructor
PhoneNumber()
{
_type = new PhoneType();
}
Or better yet, bind your combo box directly to the type instead of to the nested Type.Id (change SelectedValue Binding and remove the SelectedValuePath.
<DataGridComboBoxColumn Header="Type" Width="*" SelectedValueBinding="{Binding Mode=TwoWay, Path=Type}"
ItemsSource="{Binding Source={StaticResource PhoneTypeList}, Path=PhoneTypes}"
DisplayMemberPath="Type" />
I am trying to get a validation rule to return an error. I implemented IDataErrorInfo in my model, which contains my business object properties and messages to return in the event validation fails. I also created a validation rule. The problem is, the validation rule is firing (bookmarked it) but the IDataErrorInfo reference in the rule never has an error, even though the IDataErrorInfo implementation of my model generates one. The datagrid definitely shows there was a validation failure.
I tested it by having the rule and model return two different messages, and the model's version is always returned. It is like my rule cannot see what is in the IDataErrorInfo object, or it is just creating a new instance of it.
DataGrid:
<DataGrid ItemsSource="{Binding Path=ProjectExpenseItemsCollection}" AutoGenerateColumns="False"
Name="dgProjectExpenseItems" RowStyle="{StaticResource RowStyle}"
SelectedItem="{Binding Path=SelectedProjectExpenseItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
CanUserDeleteRows="True" CanUserAddRows="True">
<DataGrid.RowValidationRules>
<vr:RowDataInfoValidationRule ValidationStep="UpdatedValue" />
</DataGrid.RowValidationRules>
<DataGrid.Columns>
<DataGridTextColumn Header="Item Number"
Binding="{Binding ItemNumber, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
</DataGrid.Columns>
</DataGrid>
Validation Rule:
The object "idei" isn't null, but idei.Error is always a zero-length string ("")
public class RowDataInfoValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
BindingGroup bindingGroup = (BindingGroup)value;
IDataErrorInfo idei = bindingGroup.Items[0] as IDataErrorInfo;
string error = (idei != null) ? idei.Error : null;
return (string.IsNullOrEmpty(error)) ? ValidationResult.ValidResult : new ValidationResult(false, error + ": ValidationRule");
}
}
Model/Business Object:
public class ProjectExpenseItemsBO : IDataErrorInfo, IEditableObject, INotifyPropertyChanged
{
public string ItemNumber { get; set; }
public ProjectExpenseItemsBO() {}
// string method
static bool IsStringMissing(string value)
{
return String.IsNullOrEmpty(value) || value.Trim() == String.Empty;
}
#region IDataErrorInfo Members
public string Error
{
get { return this[string.Empty]; }
}
public string this[string propertyName]
{
get
{
string result = string.Empty;
if (propertyName == "ItemNumber")
{
if (IsStringMissing(this.ItemNumber))
{
result = "Item number cannot be empty-IDataError!";
}
}
return result;
}
}
#endregion
}
The IDataErrorInfo object that the rule gets will be an instance of your ProjectExpenseItemsBO object. The only property you check is Error, which you have implemented to return this[string.Empty], which will always return string.Empty. You probably either want to change your implementation of the Error property to look at all the errors in the object, or to have RowDataInfoValidationRule iterate through properties and get the error message for each one through the indexer.
You are getting validation errors from the model because your binding to ItemNumber has ValidatesOnDataErrors set to True, so the framework will call the indexer with the property name ItemNumber and get your error message.
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.