Data Grid not updating from ViewModel - wpf

Do you know why looking at the code in the XAML and ViewModel why a Data Grid wont update when the an item is added? I know the DataGrid binding is correct as will add an item if I add it by code, however I am adding a item via a view.
Also the getter of the ObservableCollection is getting hit every time I do add a item.
Thanks
------ Code ------
XAML
<Window x:Class="Importer.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Product Group" Height="350" Width="721"
xmlns:VM="clr-namespace:Importer.ViewModels">
<Window.DataContext>
<VM:ProductsViewModel />
</Window.DataContext>
<Grid Margin="0,0,0.4,0.4">
<DataGrid x:Name="dgGrid" ColumnWidth="Auto" Margin="10,10,0,0" VerticalAlignment="Top"
ItemsSource="{Binding ProductCollection}" HorizontalAlignment="Left" AutoGenerateColumns="False" >
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Name}" Header="Name" Width="150"/>
<DataGridTextColumn Binding="{Binding Description}" Header="Description" Width="400" />
</DataGrid.Columns>
</DataGrid>
<Button x:Name="btnAddProjectGroup" Content="Add Project Group" HorizontalAlignment="Left" Margin="10,277,0,0" VerticalAlignment="Top" Width="135" Click="btnAddProjectGroup_Click"/>
</Grid>
</Window>
VIEW 1 (that uses the XAML above)
public partial class MainWindow : Window
{
ProductsViewModel m_VM = new ProductsViewModel();
public MainWindow()
{
InitializeComponent();
}
private void btnAddProjectGroup_Click(object sender, RoutedEventArgs e)
{
// Open new window here to add a new project group
AddProductGroup dlg = new AddProductGroup(m_VM);
dlg.ShowDialog();
}
}
VIEW 2 - The view that passes the values to add to the collection
public partial class AddProductGroup : Window
{
ProductGroupBindable newProduct = new ProductGroupBindable();
ProductsViewModel viewModel = null;
public AddProductGroup(ProductsViewModel vm)
{
viewModel = vm;
InitializeComponent();
}
private void btnOK_Click(object sender, RoutedEventArgs e)
{
newProduct.Id = System.Guid.NewGuid();
newProduct.Name = txtName.Text;
newProduct.Description = txtDescription.Text;
viewModel.AddProduct(newProduct);
this.Close();
}
}
VIEWMODEL
private ObservableCollection<ProductGroupBindable> m_ProductCollection;
public ProductsViewModel()
{
if (m_ProductCollection == null)
m_ProductCollection = new ObservableCollection<ProductGroupBindable>();
}
public ObservableCollection<ProductGroupBindable> ProductCollection
{
get
{
return m_ProductCollection;
}
set
{
m_ProductCollection = value;
}
}
private ObservableCollection<ProductGroupBindable> Test()
{
m_ProductCollection.Add(new ProductGroupBindable { Description = "etc", Name = "test12" });
m_ProductCollection.Add(new ProductGroupBindable { Description = "etc", Name = "test123" });
return ProductCollection;
}
public void AddProduct(ProductGroupBindable newProduct)
{
m_ProductCollection.Add(newProduct);
//NotifyPropertyChanged("ProductCollection");
}

You set DataContext in XAML:
<Window.DataContext>
<VM:ProductsViewModel />
</Window.DataContext>
and your dialog works on another instance of ProductsViewModel which you create in code:
ProductsViewModel m_VM = new ProductsViewModel();
Easiest workaround would be to pass DataContext to your AddProductGroup dialog:
AddProductGroup dlg = new AddProductGroup(this.DataContext as ProductsViewModel);
EDIT
or instead of setting DataContext in XAML do set it in code:
public MainWindow()
{
InitializeComponent();
this.DataContext = m_VM;
}

Related

WPF XAML Trying to bind the Width of a DataGrid Column

I'd like to bind the Width of my Columns to a Property in my Model so I can save it if the user resize it. I'd like a solution with no code behind.
This is what I have so far:
XAML:
<DataGrid x:Name="dgArticles" AutoGenerateColumns="False" ItemsSource="{Binding Specifications.Articles}" RowDetailsVisibilityMode="Visible">
<DataGrid.Columns>
<DataGridTextColumn x:Name="Number" Header="Number" Binding="{Binding Number}" Width="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}, Path=DataContext.Specifications.Config.NumberColumnWidth}" MinWidth="70" >
Model:
public class Specifications
{
private ConfigurationGrid config
public ConfigurationGrid Config { get { return config; } set { } }
private ObservableCollection<Article> articles;
public ObservableCollection<Article> Articles
{
get { return articles; }
set { }
}
public class ConfigurationGrid : INotifyPropertyChanged
{
private double numberColumnWidth;
public double NumberColumnWidth
{
get { return numberColumnWidth; }
set { numberColumnWidth = value; OnPropertyChanged("numberColumnWidth"); }
}
public ConfigurationGrid() { }
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this,
new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
}
I managed to bind the Width of a sub Datagrid Column which is in my RowDetailsTemplate to the Width of another column like so:
<DataGridTextColumn Header="Quantity" CellStyle="{StaticResource QuantityStyle}" Binding="{Binding Quantity, UpdateSourceTrigger=PropertyChanged, StringFormat=\{0:n\}}"
Width="{Binding Source={x:Reference Mesure}, Path=ActualWidth}"/>
This works fine but I don't know why it's not working on my main DataGrid.
After debugging I noticed that it doesn't even reach the Getter of NumberColumnWidth.
Does anyone know a way to make it work? Thank you
Edit
I tried the solution provided by #mm8 but it didn't work. It's still not reaching the Getter. Maybe I missed something. Here is what the code looks like right now:
XAML:
<UserControl x:Class="CachView.Views.GridView"
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"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:local="clr-namespace:CachView.ViewModels"
xmlns:conv="clr-namespace:CachView.Converters"
xmlns:util="clr-namespace:CachView.Util"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.DataContext>
<local:ArticleViewModel/>
</UserControl.DataContext>
<Grid Margin="10">
<DataGrid x:Name="dgArticles" AutoGenerateColumns="False" ItemsSource="{Binding Specifications.Articles}" RowDetailsVisibilityMode="Visible">
<DataGrid.Resources>
<util:BindingProxy x:Key="proxy" Data="{Binding}"/>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn x:Name="Number" Header="Number" Binding="{Binding Number}" Width="{Binding Data.Specifications.Config.NumberColumnWidth, Source={StaticResource proxy}}"
MinWidth="70">
</DataGridTextColumn>
Code behind:
public partial class GridView : UserControl
{
public GridView(ArticleViewModel a)
{
InitializeComponent();
this.DataContext = a;
}
}
And my BindingProxy class is the same as in the example:
class BindingProxy : Freezable
{
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}
My project is a UserControl that is meant to be used from a WinForm application. Here is how it is implemented and how I set the DataContext and properties. It is done from the Controller of my WinForm application.
class Controller
{
private ArticleViewModel articleViewModel;
private ElementHost elementHost;
private MainWindow winformView;
public ArticleViewModel ArticleViewModel { get { return articleViewModel; } }
public Collection<Article> Articles { get; set; }
public Specifications Specs { get; set; }
public Controleur(MainWindow view) // The view is received from Program.cs
{
this.winformView = view;
Articles = new Collection<Article>();
populateArticles(); // This create hard coded articles for testing purpose
ConfigurationGrid config= new ConfigurationGrid();
config.NumberColumnWidth = 300;
Specs = new Specifications(Articles);
Specs.Config = config;
articleViewModel = new ArticleViewModel(Specs);
GridView gridView = new GridView(articleViewModel); //This is my WPF UserControl
elementHost = new ElementHost();
elementHost.Dock = DockStyle.Fill;
this.winformView.Controls.Add(elementHost);
elementHost.Child = gridView;
}
My ViewModel:
public class ArticleViewModel
{
private Specifications specifications;
public Specifications Specifications { get { return specifications; } set { } }
public ArticleViewModel() { }
public ArticleViewModel(Specifications c)
{
this.specifications = c;
}
}
Any help or suggestion is welcome.
A DataGridTextColumn is not a visual element that gets added to the element tree so you won't be able to bind to a RelativeSource since there are no ancestors to bind to.
If you want to be able to bind the Width property to a view model property you could use a BindingProxy object that captures the DataContext as suggested in the following blog post.
[WPF] How to bind to data when the DataContext is not inherited: https://www.thomaslevesque.com/2011/03/21/wpf-how-to-bind-to-data-when-the-datacontext-is-not-inherited/

How to get combobox to create a new object in response to text type in by the user?

I have items stored in the ItemSource property of a combobox, however, when the user types in a name that does not exist in the list I need it to create a new object and use that as the SelectedObject. I am pretty new to WPF and used to WindowsForms, so I might just be going about doing this the totally wrong way, any input is appreciated.
Xaml:
<Window x:Class="ComboExample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<ComboBox x:Name="cboName" IsEditable="True" SelectionChanged="ComboBox_SelectionChanged"></ComboBox>
<DockPanel>
<Label>Selected Value</Label>
<TextBox Text="{Binding Name}"></TextBox>
</DockPanel>
<Button Click="Button_Click">Click Me</Button>
</StackPanel>
and code behind (which displays "value is null" if you type a new value in
class SomeClass
{
public SomeClass(string name) {this.Name = name;}
public string Name { get; set; }
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
cboName.ItemsSource = new SomeClass[] { new SomeClass("A"), new SomeClass("B") };
cboName.DisplayMemberPath = "Name";
cboName.SelectedItem = cboName.ItemsSource.OfType<SomeClass>().FirstOrDefault();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
SomeClass value = cboName.SelectedValue as SomeClass;
if (value == null)
MessageBox.Show("No item is selected.");
else
MessageBox.Show("An item is selected.");
}
SomeClass empty = new SomeClass("");
private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
DataContext = cboName.SelectedItem as SomeClass;
if (DataContext == null)
cboName.SelectedValue = DataContext = empty;
}
}
here one way to do it:
<StackPanel>
<ComboBox x:Name="cboName" ItemsSource="{Binding ComboBoxItems}" DisplayMemberPath="Name" SelectedItem="{Binding ComboBoxItems[0],Mode=OneTime}" IsEditable="True"/>
<DockPanel>
<Label>Selected Value</Label>
<TextBox Text="{Binding SelectedItem.Name,ElementName=cboName}"></TextBox>
</DockPanel>
<Button Click="Button_Click">Click Me</Button>
</StackPanel>
and the code behind :
public partial class MainWindow : Window
{
private ObservableCollection<SomeClass> _comboBoxItems;
public ObservableCollection<SomeClass> ComboBoxItems
{
get
{
return _comboBoxItems;
}
set
{
_comboBoxItems = value;
}
}
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
ComboBoxItems = new ObservableCollection<SomeClass>()
{
new SomeClass("First Name"),
new SomeClass("Second Name"),
new SomeClass("Third Name")
};
}
private void Button_Click(object sender, RoutedEventArgs e)
{
if (!ComboBoxItems.Any(x => x.Name == cboName.Text))
{
ComboBoxItems.Add(new SomeClass(cboName.Text));
}
}
}
public class SomeClass
{
public SomeClass(string name) { this.Name = name; }
public string Name { get; set; }
}
To better manage your objects you may consider
defining a new property For the Selected ComboBox Item (Of Type SomeClass), and bind it to the ComboBox SelectedItem,
Use ObservableCollection instead of just list and Implement the INotifyPropertyChanges Interface.

Silverlight 5 Binding - Why isn't this working?

I can bind a combobox in the codebehind like this:
private void comboBox1_Loaded(object sender, RoutedEventArgs e)
{
var combo = sender as ComboBox;
App.SchedulerVM = new ScheduleViewModel();
combo.DataContext = App.SchedulerVM;
combo.ItemsSource = App.SchedulerVM.Frequency;
}
This works - my combobox has the items from the Frequency List in the SchedulerVM object.
However, I don't want to do any of this in the codebehind. But the ways I've done this in WP7 before aren't working here. If I comment out the last line in the Loaded method above and try to set the ItemsSource in XAML, it doesn't work - nothing shows up:
<ComboBox Name="comboBox1" Loaded ="comboBox1_Loaded" ItemsSource="{Binding
Frequency}" />
This doesn't work either:
<ComboBox Name="comboBox1" Loaded ="comboBox1_Loaded" ItemsSource="{Binding
App.SchedulerVM.Frequency}" />
Nor this:
<ComboBox Name="comboBox1" Loaded ="comboBox1_Loaded" ItemsSource="{Binding
SchedulerVM.Frequency}" />
Ideally, the DataContext wouldn't have to be explicitly set in the codebehind for this control either, it would be inherited from the LayoutRoot, where I've set it in the codebehind. But that's step 2 of my troubleshooting here.
What am I doing wrong? '
Thanks!
Edit
The ScheduleViewModel looks like this:
namespace SchedulerUI.ViewModels
{
public class ScheduleViewModel : INotifyPropertyChanged
{
//private properties
private Schedule _thisSchedule;
//public properties
public Schedule ThisSchedule
{
get { return _thisSchedule; }
set
{
if (value != _thisSchedule)
{
NotifyPropertyChanged("ThisSchedule");
}
_thisSchedule = value;
}
}
public List<string> Frequency = new List<string>();
public string Test;
//constructors
public ScheduleViewModel()
{
Frequency.AddRange(new string[] { "Daily", "Weekly", "Monthly" });
Test = "This is only a test.";
}
//INotifyPropertyChanged Implementation
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (null != handler)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
Here's the entire XAML:
<UserControl x:Class="SchedulerUI.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"
d:DesignHeight="300" d:DesignWidth="400">
<Grid x:Name="LayoutRoot" Background="White" Loaded="LayoutRoot_Loaded">
<ComboBox Height="23" HorizontalAlignment="Left" Margin="34,41,0,0" Name="comboBox1" Loaded ="comboBox1_Loaded" VerticalAlignment="Top" Width="120" ItemsSource="{Binding Frequency}" />
<TextBox BorderBrush="Black" HorizontalAlignment="Left" Margin="34,41,0,0" Width="100" Height="100" DataContext="LayoutRoot.DataContext" Text="{Binding Test}" />
</Grid>
</UserControl>
Here's the entire codebehind:
namespace SchedulerUI
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
App.SchedulerVM = new ScheduleViewModel();
comboBox1.DataContext = App.SchedulerVM;
List<string> testlist = App.SchedulerVM.Frequency;
string teststring = App.SchedulerVM.Test;
}
private void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
{
//App.SchedulerVM = new ScheduleViewModel();
//var root = sender as Grid;
//if (root != null)
//{
// root.DataContext = App.SchedulerVM;
//}
}
private void comboBox1_Loaded(object sender, RoutedEventArgs e)
{
//var combo = sender as ComboBox;
//App.SchedulerVM = new ScheduleViewModel();
//combo.DataContext = App.SchedulerVM;
//combo.ItemsSource = App.SchedulerVM.Frequency;
}
}
}
You binding is not working, because:
when you set ItemsSource in XAML its get executed first and it tries to bind the wrong/empty DataContext
then the Loaded event is raised which will set the correct DataContext but your already existing binding won't be refreshed automatically.
If you have to set the DataContext in the codebehind do it in your views constructor:
public YourView()
{
InitializeComponent();
combo.DataContext = App.SchedulerVM;
}
Then the following binding should work:
<ComboBox Name="comboBox1" ItemsSource="{Binding Frequency}" />
The databinding in WPF/Silverlight needs public properties. Currently Frequency is a public field on your viewmodel change it to a property and everthing should work:
private List<string> frequency = new List<string>();
public List<string> Frequency { get { return frequency; } set { frequency = value; }
And that is why it worked your initial loaded event because you didn't used databind there but you just set the combo.ItemsSource.

Binding a ComboBox entirely in XAML

I have a ComboBox on a silverlight control, that I want to bind. Sounds simple, except what I'm finding is that because the data for the ItemsSource comes from a web service asynchronously, I need to use the code behind to bind the SelectedValue only after the data has come back.
The collection that the data goes in implements INotifyCollectionChanged and INotifyPropertyChanged, so it should all be working, and indeed the combo box loads properly, but there is no value pre-selected.
What I think is happening is that the SelectedValue is getting bound before the collection has loaded - when the combobox is empty - so nothing is selected, and then later when the data comes in, the combobox is populated, but it is not checking the selected value again.
So whilst I have this working if I use code behind to hook up events and creating bindings in code, I'd like to move this all to XAML with something like:
<ComboBox HorizontalAlignment="Stretch" Margin="5,3,9,127" Name="cboCategoryID" Grid.Row="4" Grid.Column="1"
ItemsSource="{StaticResource Categories}"
SelectedValue="{Binding CategoryID, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnDataErrors=True}"
SelectedValuePath="CategoryID"
DisplayMemberPath="Caption"
VerticalAlignment="Center">
</ComboBox>
This correctly loads items, but doesn't bind the selected value. If I put the following code in the code-behind, it all works:
public MainControl()
{
InitializeComponent();
CategoryCollection cats = new CategoryCollection();
cats.Dispatcher = this.Dispatcher;
cats.LoadComplete += new EventHandler(cats_LoadComplete);
cboCategoryID.ItemsSource = cats;
cats.LoadAll();
}
private void cats_LoadComplete(object sender, EventArgs e)
{
cboCategoryID.SetBinding(ComboBox.SelectedValueProperty, new System.Windows.Data.Binding("CategoryID"));
}
Is there a way to do this without resorting to code behind?
Are you using mvvm? If so, you can try to set the ItemsSource and SelectedItem in the callback of the web service, or take a look at this post from Kyle.
http://blogs.msdn.com/b/kylemc/archive/2010/06/18/combobox-sample-for-ria-services.aspx
you are already using a collection that notifies of changes, so if the value that you are binding the SelectedValue to is notifying of changes, then all you have to do is set that property after the values are loaded from the webservice. it SHOULD update the combobox automatically, allowing you to do your binding purely in xaml.
public myObject CategoryID { get {....}
set {
this.categoryID = value;
RaisePropertyChanged("CategoryID");}
public void DataLoadedHandler()
{
CategoryID = 34; // this will cause the binding to update
}
take a look at this simple sample:
XAML:
<UserControl
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:sdk="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data" xmlns:local="clr-namespace:StackoverflowQuestions.Silverlight" xmlns:sdk1="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk" x:Class="StackoverflowQuestions.Silverlight.MainPage"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<UserControl.Resources>
<DataTemplate x:Key="Item">
<TextBlock Text="{Binding PropertyToBeWatched}" />
</DataTemplate>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White">
<!--<sdk:DataGrid ItemsSource="{Binding MyList}" RowStyle="{StaticResource Style1}">
<sdk:DataGrid.Columns>
<sdk:DataGridTextColumn Binding="{Binding PropertyToBeWatched}" Header="Property1"/>
</sdk:DataGrid.Columns>
</sdk:DataGrid>-->
<ComboBox ItemsSource="{Binding MyList}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}" Height="50" VerticalAlignment="Top" ItemTemplate="{StaticResource Item}" />
</Grid>
</UserControl>
Codebehind:
public partial class MainPage : UserControl, INotifyPropertyChanged
{
private ObservableCollection _myList;
private CustomClass _selectedItem;
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<CustomClass> MyList
{
get { return _myList ?? (_myList = new ObservableCollection<CustomClass>()); }
set
{
_myList = value;
RaisePropertyChanged("MyList");
}
}
public CustomClass SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
RaisePropertyChanged("SelectedItem");
}
}
protected void RaisePropertyChanged(string propertyname)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyname));
}
public MainPage()
{
InitializeComponent();
this.DataContext = this;
MyList.Add(new CustomClass() { PropertyToBeWatched = "1"});
MyList.Add(new CustomClass() { PropertyToBeWatched = "2" });
MyList.Add(new CustomClass() { PropertyToBeWatched = "2" });
MyList.Add(new CustomClass() { PropertyToBeWatched = "2" });
SelectedItem = MyList[1]; //Here is where it happens
}
}
By binding the SelectedItem of the ComboBox to an entity, we can achieve what you want. This works TwoWay ofcourse.
Hope this helps. :D

Binding WPF DataGrid to DataTable using TemplateColumns

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;
}
}

Resources