I am using the EntityFramework library in my WPF Application and I am having the following issue:
I am using the MVVM pattern (to the best of my knowledge) and I am trying to make a Combobox Lookup with EF values.
I have a Company class which contains many Offices (a class as well)
This has been modelled through the EntityFramework and all the links are correct (Office has a CompanyName which is a foreign key).
Here is the OfficeView class:
public partial class AddOffice : Window
{
private DBHelper.ResourceManagementContext context = new DBHelper.ResourceManagementContext();
public AddOffice()
{
InitializeComponent();
context.Companies.Load();
this.DataContext = context.Companies.Local;
//this.DataContext = new AddOfficeViewModel();
}
public void CloseCommandHandler(object sender, ExecutedRoutedEventArgs e)
{
this.Close();
}
}
Here is the corresponding XAML:
<Label Grid.Row="4" Grid.Column="0" Margin="10,10">Company:</Label>
<ComboBox Grid.Row="4" Grid.Column="1" Margin="10,10"
ItemsSource="{Binding}"
DisplayMemberPath="CompanyName"
SelectedValuePath="CompanyName"
SelectedValue="{Binding Path=CompanyName}"/>
I know the MVVM pattern usually passes a ViewModel to the View so how would I accomplish binding the EntityFramework Company list to the ComboBox using the OfficeViewModel?
I understand the ComboBox properties. I know the selected value would be the CompanyName from the Office object and the SeletecValuePath would be the CompanyName from the Company object.
In View Model:
class OfficeViewModel
{
private string _CompanyName;
public string CompanyName
{
get
{
return _CompanyName;
}
set
{
_CompanyName = value;
NotifyPropertyChanged("CompanyName");
}
}
private List<Location> _CompanyList;
public List<Location> CompanyList
{
get
{
return _CompanyList;
}
set
{
_CompanyList = value;
NotifyPropertyChanged("CompanyList");
}
}
public List<Company> GetCompanyList()
{
return (from comp in Entity.Companies select comp).ToList();
}
}
In Xaml:
Add the namespace in xaml as follows:
xmlns:ViewModels="clr-namespace:WpfMvvmApplication.ViewModels"
Add the following in window.resources:
<Window.Resources>
<ViewModels:OfficeViewModel x:Key="OfficeController"/>
</Window.Resources>
Bind the View Model to combobox:
<Label Grid.Row="4" Grid.Column="0" Margin="10,10">Company:</Label>
<ComboBox Grid.Row="4" Grid.Column="1" Margin="10,10"
ItemsSource="{Binding CompanyList, Source={StaticResource OfficeController}}"
DisplayMemberPath="CompanyName"
SelectedValuePath="CompanyName"
SelectedValue="{Binding Path=CompanyName}"/>
Hope this helps you.
Related
C#:
public void SetCompetition(Window wT1)
{
//Add all the Copetition
wT1._competition = new List<Competition>();
wT1._competition.Add(new Competition { Logo = "3.png", Name = "test1", IsSelected = false });
wT1._competition.Add(new Competition { Logo = "3.png", Name = "test2", IsSelected = false });
wT1._competition.Add(new Competition { Logo = "3.png", Name = "test3", IsSelected = false });
wT1._competition.Add(new Competition { Logo = "3.png", Name = "test4", IsSelected = false });
wT1.cboSetupCompetition.ItemsSource = wT1._competition;
wT1.cboSetupCompetition.Items.Refresh();
}
Data Template:
<UserControl.Resources>
<System:Double x:Key="Double1">11</System:Double>
<DataTemplate x:Key="cmbCompetition">
<WrapPanel Height="30" >
<Label Content="{Binding Name}" ></Label>
</WrapPanel>
</DataTemplate>
</UserControl.Resources>
<ComboBox x:Name="cboSetupCompetition" ItemTemplate="{DynamicResource cmbCompetition}" HorizontalAlignment="Left" Margin="29,28,0,0" VerticalAlignment="Top" Width="173" RenderTransformOrigin="0.5,0.591" FontSize="12" Height="22" IsEditable="True" Background="#FFD8D8D8" SelectionChanged="UpdateCompetitionSelection"/>
I have a Combobox with a label and an image and when I select an item I would like to see the same format in the Combobox when it is closed. I am not getting any errors I am seeing the name of the application.Competition(this is my object Model) instead of the values of the image and label.
The SetCopetition is invoked when the application loads.
A TextBox is not able to display a Label and an Image or whatever elements that are in your DataTemplate in it.
Set the IsEditable property of the ComboBox to false and it should work as expected, i.e. your DataTemplate will be applied to the selected item when the ComboBox is closed:
<ComboBox x:Name="cboSetupCompetition" IsEditable="False" ItemTemplate="{DynamicResource cmbCompetition}" HorizontalAlignment="Left" Margin="29,28,0,0" VerticalAlignment="Top" Width="173" RenderTransformOrigin="0.5,0.591" FontSize="12" Height="22" Background="#FFD8D8D8" SelectionChanged="UpdateCompetitionSelection"/>
Your issue has nothing to do with MVVM...
the specific problem as Mn8 spotted is that IsEditable=true forces the combo to display a textbox as the selected item
However you are still thinking winforms not WPF, using code behind to link data into the view causes many problems and instability as quite often this breaks the binding connections which is what is suspected was your problem initially, using a proper MVVM approach will eliminate all these problems
the best overveiw of MVVM i know of is
https://msdn.microsoft.com/en-gb/library/hh848246.aspx
Model
this is your data layer, it handle storage and access to data, your model will handle access to files, databases, services, etc
a simple model would be
public class Model
{
public string Text { get; set; }
public Uri Uri { get; set; }
}
ViewModel
on top of your Model you have your View Model
this manages the interaction of your View with the model
for example here because it uses Prism's BindableBase the SetProperty method notifies the View of any changes to the data, the ObservableCollection automatically notifies of changes to the collection, it also uses Prism's DelegateCommand to allow method binding in the view
public class ViewModel:BindableBase
{
public ViewModel()
{
AddItem = new DelegateCommand(() => Collection.Add(new Model()
{
Text = NewText,
Uri = new Uri(NewUri)
}));
}
private string _NewText;
public string NewText
{
get { return _NewText; }
set { SetProperty(ref _NewText, value); }
}
private string _NewUri;
public string NewUri
{
get { return _NewUri; }
set { SetProperty(ref _NewUri, value); }
}
private Model _SelectedItem;
public Model SelectedItem
{
get { return _SelectedItem; }
set
{
if (SetProperty(ref _SelectedItem, value))
{
NewText = value?.Text;
NewUri = value?.Uri.ToString();
}
}
}
public ObservableCollection<Model> Collection { get; } = new ObservableCollection<Model>();
public DelegateCommand AddItem { get; set; }
}
View
the View ideally does nothing but displays and collects data, all formatting / Styling should be done here
firstly you need to define the data source, the usual way is via the data context as this auto inherits down the visual tree, in the example because i set the window's datacontext, i have also set it for everything in the window the only exception is the dataTempplate as this is set to the current item in the collection
i then bind properties to the datasource
Note the code behind file is only the default constructor no other code at all
<Window
x:Class="WpfApplication1.MainWindow"
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:local="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:ViewModel/>
</Window.DataContext>
<StackPanel>
<GroupBox Header="Text">
<TextBox Text="{Binding NewText}"/>
</GroupBox>
<GroupBox Header="URI">
<TextBox Text="{Binding NewUri}"/>
</GroupBox>
<Button Content="Add" Command="{Binding AddItem}"/>
<ComboBox ItemsSource="{Binding Collection}" SelectedItem="{Binding SelectedItem}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Uri}" />
<TextBlock Text="{Binding Text}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</Window>
I currently have an application with a user interface in Windows Forms. The code behind this user interface communicates with a service.
For example, I have the following code:
public partial class MainWindow : Window
{
private KeyLessAccessLogic ServiceLogic;
public MainWindow()
{
InitializeComponent();
ServiceLogic = new KeyLessAccessLogic();
//LoadValues();
}
public KeyLessAccessLogic MyServiceLogic
{
get { return ServiceLogic; }
set
{
ServiceLogic = value;
// RaisePropertyChanged("MyServiceLogic");
}
}
private void BindDataSource()
{
cmb_user_name.DataSource = null;
cmb_user_name.Sorted = false;
cmb_user_name.DataSource = ServiceLogic.Users;
cmb_user_name.DisplayMember = "name";
}
And my XAML:
<ComboBox Height="23" HorizontalAlignment="Left" Margin="6,71,0,0"
Name="cmb_user_update" VerticalAlignment="Top" Width="120"
ItemsSource="{Binding Path=MyServiceLogic.Users}" DisplayMemberPath="name" />
Now I recreated the UI in WPF, and I'm a bit lost on the new format. I do believe that example I gave here is one of the examples of the difference between WPF and Windows Forms.
How can I let my application know what the datasource should be of the Dropdown-box cmb_user_name? ServiceLogic is the central block of my service, accessing for example the database.
As a second thing, I have a listbox to show me some devices. I tried to approach the datasource differently to show what else I have tried:
<ListBox Height="100" HorizontalAlignment="Left" Margin="6,44,0,0"
Name="listBox_detected" VerticalAlignment="Top" Width="120"
ItemsSource="{Binding Path=ServiceLogic.TheDevicesList}" DisplayMemberPath="name" />
Use XAML for that:
<ComboBox ItemsSource="{Binding MyServiceLogic.Users}"
SelectedItem="{Binding User}"
DisplayMemberPath="name" />
Create a property ServiceLogic in your ViewModel to hold a ServiceLogic object:
private ServiceLogic myServiceLogic;
public ServiceLogic MyServiceLogic
{
get { return myServiceLogic; }
set
{
myServiceLogic = value;
RaisePropertyChanged("MyServiceLogic");
}
}
I assume Users is ObservableCollection. Or you can create a property which holds Users collection directly.
I have a combobox that is editable by the user, so I bound the Text property to a property of my class. The ItemsSource of that same combobox is bound to an AsyncObservableCollection (which I did based on other posts and it works nicely).
However, I have a problem when updating the ItemsSource.
Here's the Steps to reproduce:
Select a value in the combobox drop down.
Type some text into the combobox. (say "aaa")
Update the ItemsSource. (via my button click)
Result: The MyText property remains set to the text you typed in ("aaa"), but the combo box shows a blank entry.
However, if you do the same steps above but skip Step 1, the combobox shows the text from the MyText property correctly. This leads me to believe that the selected index/selected value is being used to update the combobox after the update to the ItemsSource is done.
Any ideas on how I can keep the displayed value in sync with the MyText property after an update to the ItemsSource?
In the code provided below I'm updating the ItemsSource on the button click in order to reproduce.
Thank You!
XAML:
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded">
<Grid>
<ComboBox Height="23" HorizontalAlignment="Left" Margin="12,12,0,0" Name="comboBox1" VerticalAlignment="Top" Width="200" IsEditable="True"
DataContext="{Binding Path=MyDataClass}"
ItemsSource="{Binding Path=MyListOptions}"
SelectedIndex="{Binding Path=MySelectedIndex}"
Text="{Binding Path=MyText, UpdateSourceTrigger=LostFocus}"
>
</ComboBox>
<Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="416,276,0,0" Name="button1" VerticalAlignment="Top" Width="75" Click="button1_Click" />
</Grid>
Code behind:
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Diagnostics;
namespace WpfApplication2
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public class DataClass : INotifyPropertyChanged
{
private string mytext = "";
public string MyText
{
get
{
return mytext;
}
set
{
mytext = value;
OnPropertyChanged("MyText");
}
}
private int myselectedindex = -1;
public int MySelectedIndex
{
get
{
return myselectedindex;
}
set
{
if (value != -1)
{
mytext = MyListOptions[value];
OnPropertyChanged("MyText");
}
}
}
private AsyncObservableCollection<string> mylistOptions = new AsyncObservableCollection<string>();
public AsyncObservableCollection<string> MyListOptions
{
get
{
return mylistOptions;
}
set
{
mylistOptions.Clear();
OnPropertyChanged("MyListOptions");
foreach (string opt in value)
{
mylistOptions.Add(opt);
}
OnPropertyChanged("MyListOptions");
}
}
public DataClass()
{
}
public event PropertyChangedEventHandler PropertyChanged;
internal void OnPropertyChanged(string prop)
{
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
}
public DataClass MyDataClass { get; set; }
public MainWindow()
{
MyDataClass = new DataClass();
MyDataClass.MyListOptions.Add("Option 1 - Provides helpful stuff.");
MyDataClass.MyListOptions.Add("Option 2 - Provides more helpful stuff.");
MyDataClass.MyListOptions.Add("Option 3 - Provides extra helpful stuff.");
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
}
private void button1_Click(object sender, RoutedEventArgs e)
{
AsyncObservableCollection<string> newList = new AsyncObservableCollection<string>();
newList.Add("Option A - Provides helpful stuff.");
newList.Add("Option B - Provides more helpful stuff.");
newList.Add("Option C - Provides extra helpful stuff.");
MyDataClass.MyListOptions = newList;
}
}
}
Ok, I solved this problem by binding the SelectedValue to the same property as Text and setting its mode to OneWay.
<ComboBox Height="23" HorizontalAlignment="Left" Margin="12,12,0,0" Name="comboBox1" VerticalAlignment="Top" Width="200" IsEditable="True"
DataContext="{Binding Path=MyDataClass}"
ItemsSource="{Binding Path=MyListOptions}"
SelectedIndex="{Binding Path=MySelectedIndex}"
SelectedValue="{Binding Path=MyText, Mode=OneWay}"
Text="{Binding Path=MyText, UpdateSourceTrigger=LostFocus}"
I have tried everything and got nowhere so I'm hoping someone can give me the aha moment.
I simply cannot get the binding to pull the data in the datagrid successfully.
I have a DataTable that contains multiple columns with of MyDataType
public class MyData
{
string nameData {get;set;}
bool showData {get;set;}
}
MyDataType has 2 properties (A string, a boolean)
I have created a test DataTable
DataTable GetDummyData()
{
DataTable dt = new DataTable("Foo");
dt.Columns.Add(new DataColumn("AnotherColumn", typeof(MyData)));
dt.Rows.Add(new MyData("Row1C1", true));
dt.Rows.Add(new MyData("Row2C1", false));
dt.AcceptChanges();
return dt;
}
I have a WPF DataGrid which I want to show my DataTable.
But all I want to do is to change how each cell is rendered to show [TextBlock][Button] per cell with values bound to the MyData object and this is where I'm having a tonne of trouble.
My XAML looks like this
<Window.Resources>
<ResourceDictionary>
<DataTemplate x:Key="MyDataTemplate" DataType="MyData">
<StackPanel Orientation="Horizontal" >
<Button Background="Green" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="5,0,0,0" Content="{Binding Path=nameData}"></Button>
<TextBlock Background="Green" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="5,0,0,0" Text="{Binding Path=nameData}"></TextBlock>
</StackPanel>
</DataTemplate>
</ResourceDictionary>
</Window.Resources>
<Grid>
<dg:DataGrid Grid.Row="1" ItemsSource="{Binding}" AutoGenerateColumns="True"
x:Name="dataGrid1" SelectionMode="Single" CanUserAddRows="False"
CanUserSortColumns="true" CanUserDeleteRows="False" AlternatingRowBackground="AliceBlue"
AutoGeneratingColumn="dataGrid1_AutoGeneratingColumn" />
</Grid>
Now all I do once loaded is to attempt to bind the DataTable to the WPF DataGrid
dt = GetDummyData();
dataGrid1.ItemsSource = dt.DefaultView;
The TextBlock and Button show up, but they don't bind, which leaves them blank.
Could anyone let me know if they have any idea how to fix this.
This should be simple, thats what Microsoft leads us to believe.
I have set the Column.CellTemplate during the AutoGenerating event and still get no binding.
Please help!!!
Edit: Updated to reflect the input of Aran Mulholland (see comment)
Apparently the DataGrid is passing the entire DataRowView to each cell. That's why the binding doesn't work. Your DataTemplate expects the DataContext to be of type MyData, but instead it is of type DataRowView. My proposed (somewhat hack-ish) workaround to get the DataContext you want is to create a custom DataGridTemplateColumn that will extract the necessary item from the DataRowView. The code is below:
<Window x:Class="DataGridTemplateColumnSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:dg="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit"
Title="Window1" Height="300" Width="300">
<Window.Resources>
<ResourceDictionary>
<DataTemplate x:Key="MyDataTemplate" DataType="DataRowView">
<StackPanel Orientation="Horizontal">
<Button Background="Green" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="5,0,0,0" Content="{Binding Path=nameData}"></Button>
<TextBlock Background="Green" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="5,0,0,0" Text="{Binding Path=nameData}"></TextBlock>
</StackPanel>
</DataTemplate>
</ResourceDictionary>
</Window.Resources>
<Grid>
<dg:DataGrid Grid.Row="1" AutoGenerateColumns="True" x:Name="dataGrid1" SelectionMode="Single"
CanUserAddRows="False" CanUserSortColumns="true" CanUserDeleteRows="False"
AlternatingRowBackground="AliceBlue" AutoGeneratingColumn="dataGrid1_AutoGeneratingColumn"
ItemsSource="{Binding}" VirtualizingStackPanel.VirtualizationMode="Standard" />
</Grid>
</Window>
using System.Data;
using System.Windows;
using Microsoft.Windows.Controls;
namespace DataGridTemplateColumnSample
{
public partial class Window1
{
public Window1()
{
InitializeComponent();
DataContext = GetDummyData().DefaultView;
}
private static DataTable GetDummyData()
{
var dt = new DataTable("Foo");
dt.Columns.Add(new DataColumn("OneColumn", typeof(MyData)));
dt.Columns.Add(new DataColumn("AnotherColumn", typeof(MyData)));
dt.Rows.Add(new MyData("Row1C1", true), new MyData("Row1C2", true));
dt.Rows.Add(new MyData("Row2C1", false), new MyData("Row2C2", true));
dt.AcceptChanges();
return dt;
}
private void dataGrid1_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
var column = new DataRowColumn(e.PropertyName);
column.Header = e.Column.Header;
column.CellTemplate = (DataTemplate)Resources["MyDataTemplate"];
e.Column = column;
}
}
public class DataRowColumn : DataGridTemplateColumn
{
public DataRowColumn(string column) { ColumnName = column; }
public string ColumnName { get; private set; }
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
var row = (DataRowView) dataItem;
var item = row[ColumnName];
cell.DataContext = item;
var element = base.GenerateElement(cell, item);
return element;
}
}
public class MyData
{
public MyData(string name, bool data) { nameData = name; showData = data; }
public string nameData { get; set; }
public bool showData { get; set; }
}
}
Note: This approach only appears to work with container virtualization off or in Standard mode. If the VirtualizationMode is set to Recycling the template is not applied.
After finding this thread and having trouble with the code shown here, I ran across this thread on MSDN, and it works much better! No virtualization problems at all so far as I've seen.
http://social.msdn.microsoft.com/Forums/en/wpf/thread/8b2e94b7-3c44-4642-8acc-851de5285062
Code:
private void dataGrid1_AutoGeneratingColumn(object sender, Microsoft.Windows.Controls.DataGridAutoGeneratingColumnEventArgs e)
{
if (e.PropertyType == typeof(MyData))
{
MyDataGridTemplateColumn col = new MyDataGridTemplateColumn();
col.ColumnName = e.PropertyName; // so it knows from which column to get MyData
col.CellTemplate = (DataTemplate)FindResource("MyDataTemplate");
e.Column = col;
e.Column.Header = e.PropertyName;
}
}
public class MyDataGridTemplateColumn : DataGridTemplateColumn
{
public string ColumnName
{
get;
set;
}
protected override System.Windows.FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
// The DataGridTemplateColumn uses ContentPresenter with your DataTemplate.
ContentPresenter cp = (ContentPresenter)base.GenerateElement(cell, dataItem);
// Reset the Binding to the specific column. The default binding is to the DataRowView.
BindingOperations.SetBinding(cp, ContentPresenter.ContentProperty, new Binding(this.ColumnName));
return cp;
}
}
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.