WPF MVVM Dynamic Columns Multi-Threaded - c#

I am currently working on a C# System.Windows.Controls.DataGrid that needs to generate the columns dynamically depending on the data. It can add and/or remove columns during runtime.
I am using a Thread in the ViewModel class to update the ObservableCollection that feeds the DataGrid.
I have read that post which explains the best solution I have found for my problem. Although, the Columns.CollectionChanged Delegate from the DataGridExtension class throws a InvalideOperationException : The calling thread cannot access this object because a different thread owns it.
Heres some code to picture it all :
The View XAML
<DataGrid ItemsSource="{Binding CollectionView, Source={StaticResource ViewModel}}" local:DataGridExtension.Columns="{Binding DataGridColumns, Source={StaticResource ViewModel}}" AutoGenerateColumns="False" Name="dataGrid">
ViewModel Class
public ObservableCollection<DataGridColumn> DataGridColumns
{
get { return columns; }
set { columns = value; }
}
private void getViewData()
{
while (true)
{
Thread.Sleep(1000);
foreach (DataObject data in dataObjects)
{
int index = -1;
foreach (DataGridColumn c in columns)
{
if (c.Header.Equals(column.Header))
index = columns.IndexOf(c);
}
DataGridColumn column = new DataGridTextColumn();
... Creating the column based on data from DataObject ...
DataGridExtension._currentDispatcher = Dispatcher.CurrentDispatcher;
if (index == -1)
{
this.columns.Add(column);
}
else
{
this.columns.RemoveAt(index);
this.columns.Add(column);
}
}
}
}
DataGridExtension class
public static class DataGridExtension
{
public static Dispatcher _currentDispatcher;
public static readonly DependencyProperty ColumnsProperty =
DependencyProperty.RegisterAttached("Columns",
typeof(ObservableCollection<DataGridColumn>),
typeof(DataGridExtension),
new UIPropertyMetadata(new ObservableCollection<DataGridColumn>(), OnDataGridColumnsPropertyChanged));
private static void OnDataGridColumnsPropertyChanged(DependencyObject iObj, DependencyPropertyChangedEventArgs iArgs)
{
if (iObj.GetType() == typeof(DataGrid))
{
DataGrid myGrid = iObj as DataGrid;
ObservableCollection<DataGridColumn> Columns = (ObservableCollection<DataGridColumn>)iArgs.NewValue;
if (Columns != null)
{
myGrid.Columns.Clear();
if (Columns != null && Columns.Count > 0)
{
foreach (DataGridColumn dataGridColumn in Columns)
{
myGrid.Columns.Add(dataGridColumn);
}
}
Columns.CollectionChanged += delegate(object sender, NotifyCollectionChangedEventArgs args)
{
if (args.NewItems != null)
{
UserControl control = ((UserControl)((Grid)myGrid.Parent).Parent);
foreach (DataGridColumn column in args.NewItems.Cast<DataGridColumn>())
{
/// This is where I tried to fix the exception. ///
DataGridColumn temp = new DataGridTextColumn();
temp.Header = column.Header;
temp.SortMemberPath = column.SortMemberPath;
control.Dispatcher.Invoke(new Action(delegate()
{
myGrid.Columns.Add(temp);
}), DispatcherPriority.Normal);
////////////////////////////////////////////////////
}
}
if (args.OldItems != null)
{
foreach (DataGridColumn column in args.OldItems.Cast<DataGridColumn>())
{
myGrid.Columns.Remove(column);
}
}
};
}
}
}
public static ObservableCollection<DataGridColumn> GetColumns(DependencyObject iObj)
{
return (ObservableCollection<DataGridColumn>)iObj.GetValue(ColumnsProperty);
}
public static void SetColumns(DependencyObject iObj, ObservableCollection<DataGridColumn> iColumns)
{
iObj.SetValue(ColumnsProperty, iColumns);
}
}
The section where I put /// This is where I tried to fix the exception. /// is where the exception is getting thrown, exactly at myGrid.add(...);
The myGrid object does not allow me to add that column to be added to the collection of columns of the DataGrid. Which is why I surrounded it with a Dispatcher.Invoke. Strangely, if I execute myGrid.Columns.Add(new DataGridTextColumn()); it works and I can see the empty columns getting added in the view but myGrid.Columns.Add(temp); throws the exception.
There must be something I don't catch with this thing.
Please HELP!!!!
EDIT following Stipo suggestion
UserControl control = ((UserControl)((Grid)myGrid.Parent).Parent);
Columns.CollectionChanged += delegate(object sender, NotifyCollectionChangedEventArgs args)
{
control.Dispatcher.Invoke(new Action(delegate()
{
if (args.NewItems != null)
{
foreach (DataGridColumn column in args.NewItems.Cast<DataGridColumn>())
{
DataGridColumn temp = new DataGridTextColumn();
temp.Header = column.Header;
temp.SortMemberPath = column.SortMemberPath;
myGrid.Columns.Add(temp);
}
}
if (args.OldItems != null)
{
foreach (DataGridColumn column in args.OldItems.Cast<DataGridColumn>())
{
myGrid.Columns.Remove(column);
}
}
}), DispatcherPriority.Normal);
};

Move DataGridColumn creation code into the dispatcher delegate.
The issue happens because DataGridColumn inherits from DispatcherObject which has one field which says on which thread the DispatcherObject was created and when DataGridColumn is constructed this field will be set to your worker thread.
When that column gets added to DataGrid.Columns collection, exception will be thrown because DataGridColumn is not created on default GUI thread on which the DataGrid is created.
NEW SOLUTION
After playing around with your code, I have decided to implement different solution which should solve your problem and make your view model cleaner since it won't have GUI members (DataGridColumns) in it anymore.
New solution abstracts DataGridColumn in view model layer with ItemProperty class and DataGridExtension class takes care of converting ItemProperty instance to DataGridColumn instance in WPF's Dispatcher thread.
Here is a complete solution with test example (I recommend you create an empty WPF Application project and insert code in it to test the solution):
ItemProperty.cs
using System;
namespace WpfApplication
{
// Abstracts DataGridColumn in view-model layer.
class ItemProperty
{
public Type PropertyType { get; private set; }
public string Name { get; private set; }
public bool IsReadOnly { get; private set; }
public ItemProperty(Type propertyType, string name, bool isReadOnly)
{
this.PropertyType = propertyType;
this.Name = name;
this.IsReadOnly = isReadOnly;
}
}
}
DataGridExtension.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
using System.Windows.Threading;
namespace WpfApplication
{
static class DataGridExtension
{
private static readonly DependencyProperty ColumnBinderProperty = DependencyProperty.RegisterAttached("ColumnBinder", typeof(ColumnBinder), typeof(DataGridExtension));
public static readonly DependencyProperty ItemPropertiesProperty = DependencyProperty.RegisterAttached(
"ItemProperties",
typeof(ObservableCollection<ItemProperty>),
typeof(DataGridExtension), new PropertyMetadata((d, e) =>
{
var dataGrid = d as DataGrid;
if (dataGrid != null)
{
var columnBinder = dataGrid.GetColumnBinder();
if (columnBinder != null)
columnBinder.Dispose();
var itemProperties = e.NewValue as ObservableCollection<ItemProperty>;
dataGrid.SetColumnBinder(new ColumnBinder(dataGrid.Dispatcher, dataGrid.Columns, itemProperties));
}
}));
[AttachedPropertyBrowsableForType(typeof(DataGrid))]
[DependsOn("ItemsSource")]
public static ObservableCollection<ItemProperty> GetItemProperties(this DataGrid dataGrid)
{
return (ObservableCollection<ItemProperty>)dataGrid.GetValue(ItemPropertiesProperty);
}
public static void SetItemProperties(this DataGrid dataGrid, ObservableCollection<ItemProperty> itemProperties)
{
dataGrid.SetValue(ItemPropertiesProperty, itemProperties);
}
private static ColumnBinder GetColumnBinder(this DataGrid dataGrid)
{
return (ColumnBinder)dataGrid.GetValue(ColumnBinderProperty);
}
private static void SetColumnBinder(this DataGrid dataGrid, ColumnBinder columnBinder)
{
dataGrid.SetValue(ColumnBinderProperty, columnBinder);
}
// Takes care of binding ItemProperty collection to DataGridColumn collection.
// It derives from TypeConverter so it can access SimplePropertyDescriptor class which base class (PropertyDescriptor) is used in DataGrid.GenerateColumns method to inspect if property is read-only.
// It must be stored in DataGrid (via ColumnBinderProperty attached dependency property) because previous binder must be disposed (CollectionChanged handler must be removed from event), otherwise memory-leak might occur.
private class ColumnBinder : TypeConverter, IDisposable
{
private readonly Dispatcher dispatcher;
private readonly ObservableCollection<DataGridColumn> columns;
private readonly ObservableCollection<ItemProperty> itemProperties;
public ColumnBinder(Dispatcher dispatcher, ObservableCollection<DataGridColumn> columns, ObservableCollection<ItemProperty> itemProperties)
{
this.dispatcher = dispatcher;
this.columns = columns;
this.itemProperties = itemProperties;
this.Reset();
this.itemProperties.CollectionChanged += this.OnItemPropertiesCollectionChanged;
}
private void Reset()
{
this.columns.Clear();
foreach (var column in GenerateColumns(itemProperties))
this.columns.Add(column);
}
private static IEnumerable<DataGridColumn> GenerateColumns(IEnumerable<ItemProperty> itemProperties)
{
return DataGrid.GenerateColumns(new ItemProperties(itemProperties));
}
private void OnItemPropertiesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// CollectionChanged is handled in WPF's Dispatcher thread.
this.dispatcher.Invoke(new Action(() =>
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
int index = e.NewStartingIndex >= 0 ? e.NewStartingIndex : this.columns.Count;
foreach (var column in GenerateColumns(e.NewItems.Cast<ItemProperty>()))
this.columns.Insert(index++, column);
break;
case NotifyCollectionChangedAction.Remove:
if (e.OldStartingIndex >= 0)
for (int i = 0; i < e.OldItems.Count; ++i)
this.columns.RemoveAt(e.OldStartingIndex);
else
this.Reset();
break;
case NotifyCollectionChangedAction.Replace:
if (e.OldStartingIndex >= 0)
{
index = e.OldStartingIndex;
foreach (var column in GenerateColumns(e.NewItems.Cast<ItemProperty>()))
this.columns[index++] = column;
}
else
this.Reset();
break;
case NotifyCollectionChangedAction.Reset:
this.Reset();
break;
}
}));
}
public void Dispose()
{
this.itemProperties.CollectionChanged -= this.OnItemPropertiesCollectionChanged;
}
// Used in DataGrid.GenerateColumns method so that .NET takes care of generating columns from properties.
private class ItemProperties : IItemProperties
{
private readonly ReadOnlyCollection<ItemPropertyInfo> itemProperties;
public ItemProperties(IEnumerable<ItemProperty> itemProperties)
{
this.itemProperties = new ReadOnlyCollection<ItemPropertyInfo>(itemProperties.Select(itemProperty => new ItemPropertyInfo(itemProperty.Name, itemProperty.PropertyType, new ItemPropertyDescriptor(itemProperty.Name, itemProperty.PropertyType, itemProperty.IsReadOnly))).ToArray());
}
ReadOnlyCollection<ItemPropertyInfo> IItemProperties.ItemProperties
{
get { return this.itemProperties; }
}
private class ItemPropertyDescriptor : SimplePropertyDescriptor
{
public ItemPropertyDescriptor(string name, Type propertyType, bool isReadOnly)
: base(null, name, propertyType, new Attribute[] { isReadOnly ? ReadOnlyAttribute.Yes : ReadOnlyAttribute.No })
{
}
public override object GetValue(object component)
{
throw new NotSupportedException();
}
public override void SetValue(object component, object value)
{
throw new NotSupportedException();
}
}
}
}
}
}
Item.cs (used for testing)
using System;
namespace WpfApplication
{
class Item
{
public string Name { get; private set; }
public ItemKind Kind { get; set; }
public bool IsChecked { get; set; }
public Uri Link { get; set; }
public Item(string name)
{
this.Name = name;
}
}
enum ItemKind
{
ItemKind1,
ItemKind2,
ItemKind3
}
}
ViewModel.cs (used for testing)
using System;
using System.Collections.ObjectModel;
using System.Threading;
namespace WpfApplication
{
class ViewModel
{
public ObservableCollection<Item> Items { get; private set; }
public ObservableCollection<ItemProperty> ItemProperties { get; private set; }
public ViewModel()
{
this.Items = new ObservableCollection<Item>();
this.ItemProperties = new ObservableCollection<ItemProperty>();
for (int i = 0; i < 1000; ++i)
this.Items.Add(new Item("Name " + i) { Kind = (ItemKind)(i % 3), IsChecked = (i % 2) == 1, Link = new Uri("http://www.link" + i + ".com") });
}
private bool testStarted;
// Test method operates on another thread and it will first add all columns one by one in interval of 1 second, and then remove all columns one by one in interval of 1 second.
// Adding and removing will be repeated indefinitely.
public void Test()
{
if (this.testStarted)
return;
this.testStarted = true;
ThreadPool.QueueUserWorkItem(state =>
{
var itemProperties = new ItemProperty[]
{
new ItemProperty(typeof(string), "Name", true),
new ItemProperty(typeof(ItemKind), "Kind", false),
new ItemProperty(typeof(bool), "IsChecked", false),
new ItemProperty(typeof(Uri), "Link", false)
};
bool removing = false;
while (true)
{
Thread.Sleep(1000);
if (removing)
{
if (this.ItemProperties.Count > 0)
this.ItemProperties.RemoveAt(this.ItemProperties.Count - 1);
else
removing = false;
}
else
{
if (this.ItemProperties.Count < itemProperties.Length)
this.ItemProperties.Add(itemProperties[this.ItemProperties.Count]);
else
removing = true;
}
}
});
}
}
}
MainWindow.xaml (used for testing)
<Window x:Class="WpfApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication">
<Window.DataContext>
<local:ViewModel/>
</Window.DataContext>
<DockPanel>
<Button DockPanel.Dock="Top" Content="Test" Click="OnTestButtonClicked"/>
<DataGrid ItemsSource="{Binding Items}" local:DataGridExtension.ItemProperties="{Binding ItemProperties}" AutoGenerateColumns="False"/>
</DockPanel>
</Window>
MainWindow.xaml.cs (used for testing)
using System.Windows;
namespace WpfApplication
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void OnTestButtonClicked(object sender, RoutedEventArgs e)
{
((ViewModel)this.DataContext).Test();
}
}
}

WPF Extension (found in codeplex) has a extended version of ObservableCollection called DispatchedObservableCollection here , which ideal here. Its worth having a look at it and customize accordingly.

Related

C# - ReportViewer - make class not static

I have a class DependencyReportViewer that is correspond for creation a ReportViewer control in my program. This class works perfectly. In XAML we define only this line for each report that we wants:
<ctr:DependencyReportViewer EmbeddedReportName="Program.View.Reports.ReportName.rdlc" ReportData="{Binding ReportData}"/>
First part of the class:
<WindowsFormsHost x:Class="Program.View.Controls.DependencyReportViewer"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
IsVisibleChanged="WindowsFormsHost_IsVisibleChanged">
</WindowsFormsHost>
And the worst part of the class:
public partial class DependencyReportViewer : WindowsFormsHost
{
private static ReportViewer ReportViewer = new ReportViewer();
private static int ReportIsRunning = 0;
static DependencyReportViewer()
{
DependencyReportViewer.ReportViewer.SetDisplayMode(DisplayMode.PrintLayout);
try
{
RenderingExtensions = ReportViewer.LocalReport.ListRenderingExtensions();
}
catch (Exception)
{
RenderingExtensions = new RenderingExtension[0];
}
ReportViewer.ReportError += ReportViewer_ReportError;
}
public static RenderingExtension[] RenderingExtensions { get; private set; }
public static readonly RoutedEvent ReportDoneEvent = EventManager.RegisterRoutedEvent("ReportDone", RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(DependencyReportViewer));
public event RoutedEventHandler ReportDone
{
add { AddHandler(ReportDoneEvent, value); }
remove { RemoveHandler(ReportDoneEvent, value); }
}
public string EmbeddedReportName
{
get { return (string)GetValue(EmbeddedReportNameProperty); }
set { SetValue(EmbeddedReportNameProperty, value); }
}
public static readonly DependencyProperty EmbeddedReportNameProperty = DependencyProperty.Register("EmbeddedReportName",
typeof(string), typeof(DependencyReportViewer));
public Tuple<IEnumerable<Tuple<string, IEnumerable>>, IEnumerable<Tuple<string, string>>> ReportData
{
get { return (Tuple<IEnumerable<Tuple<string, IEnumerable>>, IEnumerable<Tuple<string, string>>>)GetValue(ReportDataProperty); }
set { SetValue(ReportDataProperty, value); }
}
public static readonly DependencyProperty ReportDataProperty = DependencyProperty.Register("ReportData",
typeof(Tuple<IEnumerable<Tuple<string, IEnumerable>>, IEnumerable<Tuple<string, string>>>), typeof(DependencyReportViewer),
new FrameworkPropertyMetadata(null, ReportData_Changed));
private static void ReportData_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var sts = Interlocked.CompareExchange(ref ReportIsRunning, 1, 0);
if (sts != 0)
return;
var rv = (DependencyReportViewer)d;
var data = (Tuple<IEnumerable<Tuple<string, IEnumerable>>, IEnumerable<Tuple<string, string>>>)e.NewValue;
RenderingCompleteEventHandler evt = null;
evt = (sender, rea) =>
{
ReportViewer.RenderingComplete -= evt;
sts = Interlocked.Exchange(ref ReportIsRunning, 0);
if (sts == 1)
rv.RaiseEvent(new RoutedEventArgs(ReportDoneEvent));
};
ReportViewer.RenderingComplete += evt;
DependencyReportViewer.ReportViewer.Reset();
DependencyReportViewer.ReportViewer.LocalReport.ReportEmbeddedResource = null;
if (data != null)
{
DependencyReportViewer.ReportViewer.LocalReport.SetBasePermissionsForSandboxAppDomain(new System.Security.PermissionSet(System.Security.Permissions.PermissionState.Unrestricted));
DependencyReportViewer.ReportViewer.LocalReport.ReportEmbeddedResource = rv.EmbeddedReportName;
var l = new List<ReportParameter>();
foreach (var item in data.Item2)
{
l.Add(new ReportParameter(item.Item1, item.Item2));
}
DependencyReportViewer.ReportViewer.LocalReport.SetParameters(l);
foreach (var item in data.Item1)
{
var rds = new ReportDataSource(item.Item1);
rds.Value = item.Item2;
DependencyReportViewer.ReportViewer.LocalReport.DataSources.Add(rds);
}
}
DependencyReportViewer.ReportViewer.RefreshReport();
}
private static void ReportViewer_ReportError(object sender, ReportErrorEventArgs e)
{
Interlocked.Exchange(ref ReportIsRunning, 0);
}
public DependencyReportViewer()
{
InitializeComponent();
}
private void WindowsFormsHost_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if ((bool)e.NewValue)
{
this.Child = DependencyReportViewer.ReportViewer;
}
else
{
DependencyReportViewer.ReportViewer.Reset();
DependencyReportViewer.ReportViewer.LocalReport.ReportEmbeddedResource = null;
DependencyReportViewer.ReportViewer.LocalReport.DataSources.Clear();
DependencyReportViewer.ReportViewer.RefreshReport();
this.Child = null;
}
}
}
The problem is that this code works normally only in situations, when we have only one UserControl with this ReportViewer opened. If we open two or more controls, then the last open control will take over ReportViewer. In other words it is possible to open only one ReportViewer at the same time.
Now I want to have a possibility to open multiple controls with multiple report viewers. As I get the problem is that this class is static. And when we define a new control, then in "this.Child = DependencyReportViewer.ReportViewer;" line we just reset this viewer to the newest control.
But the man who wrote this had a skill much greater then I have now. I failed multiple times to set this class nonstatic with XamlParseException. How can I convert this class to nonstatic?

TextBlock Inline Binding using DependencyProperty [duplicate]

This question already has answers here:
DependencyProperty getter/setter not being called
(2 answers)
Closed 7 years ago.
I'm using MVVMLight. This is what I have from an example here.
private ObservableCollection<Inline> _inlineList;
public ObservableCollection<Inline> InlineList
{
get { return _inlineList; }
set { Set(() => InlineList, ref _inlineList, value); }
}
private void SendClicked()
{
InlineList.Add(new Run("This is some bold text") { FontWeight = FontWeights.Bold });
InlineList.Add(new Run("Some more text"));
InlineList.Add(new Run("This is some text") { TextDecorations = TextDecorations.Underline });
}
public class BindableTextBlock : TextBlock
{
public ObservableCollection<Inline> InlineList
{
get { return (ObservableCollection<Inline>)GetValue(InlineListProperty); }
set { SetValue(InlineListProperty, value); }
}
public static readonly DependencyProperty InlineListProperty =
DependencyProperty.Register("InlineList", typeof(ObservableCollection<Inline>), typeof(BindableTextBlock), new UIPropertyMetadata(null, OnPropertyChanged));
private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue == null) return;
var textBlock = (BindableTextBlock)sender;
textBlock.Inlines.AddRange((ObservableCollection<Inline>)e.NewValue);
}
}
<testRobot:BindableTextBlock
Width="Auto" Height="Auto"
InlineList="{Binding InlineList}" >
</testRobot:BindableTextBlock>
The problem is that bound property InlineList never gets updated. I don't see any text I add to the collection ObservableCollection. When I put a break point in OnPropertyChanged method it never gets hit. I know my data context is set correctly as other bound controls work.
What could be the problem?
Ok you only need to add these in your BindableTextBlock With your Original solution. What we do here is we add handler for when collection is changed (meaning new values are added), we only do that when the collection is set. So with the binding that you have in your xaml every change you make on the collection in the VM fires collection changed event in the textblock which in turn just appends values to the Inline.
private void CollectionChangedHandler(object sender, NotifyCollectionChangedEventArgs e)
{
Inlines.AddRange(e.NewItems);
}
private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue == null) return;
var textBlock = (BindableTextBlock)sender;
textBlock.InlineList.CollectionChanged += textBlock.CollectionChangedHandler;
}
BEFORE EDIT for history reasons
Ok I saw what's happening so first the explanation then an example.
So first some basic concepts about wpf:
In order to have your view notified for a change in bound variable in your ViewModel (or whatever that is DataContext at the moment) you have to either RaisePropertyChanged event with the name of the changed property or use something that's doing this somehow :) - like ObservableCollection.
So some use cases:
You have a property with field -> it is common practice to have it like this:
private ICollection<Inline> _inlineList;
public ICollection<Inline> InlineList
{
get
{
return _inlineList;
}
set
{
_inlineList = value;
RaisePropertyChanged("InlineList");
}
}
This ensures that when you set a new value to InlineList the view will be notified
Or in your case what I've used:
private ICollection<Inline> _inlineList;
public ICollection<Inline> InlineList
{
get { return _inlineList; }
set { Set(() => InlineList, ref _inlineList, value); }
}
If you check the description of Set method you'll see that it is setting the value and raising the property (and some more stuff)
You want to have automatic updates and use ObservableCollection -> I use it like this:
private ObservableCollection<ClientFilter> clientFilters;
public IEnumerable<ClientFilter> ClientFilters
{
get
{
if (this.clientFilters == null)
{
this.clientFilters = new ObservableCollection<ClientFilter>();
}
return this.clientFilters;
}
set
{
if (this.clientFilters == null)
{
this.clientFilters = new ObservableCollection<ClientFilter>();
}
SetObservableValues<ClientFilter>(this.clientFilters, value);
}
}
The method SetObservableValues is in my main ViewModel and is doing this:
public static void SetObservableValues<T>(
ObservableCollection<T> observableCollection,
IEnumerable<T> values)
{
if (observableCollection != values)
{
observableCollection.Clear();
foreach (var item in values)
{
observableCollection.Add(item);
}
}
}
This method ensures that if the reference to the obs collection is not the same it will clear the old one and reuse it, because when you bind you bind to the reference at common mistake is to then change the reference itself not the values, which in turn doesn't update anything on the UI and you think binding is broken :)
So If you want it to function normally you just add/remove to the Collection/Enumerable ClientFilters
Now the solution
So I'm not 100% sure what you want to achieve but here's what you could do in order to have your binding working
Your ViewModel
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Documents;
namespace WpfApplication3.ViewModel
{
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
}
private ICollection<Inline> _inlineList;
public ICollection<Inline> InlineList
{
get { return _inlineList; }
set { Set(() => InlineList, ref _inlineList, value); }
}
public RelayCommand SendClicked
{
get
{
return new RelayCommand(() =>
{
InlineList = new List<Inline>
{
new Run("This is some bold text") { FontWeight = FontWeights.Bold },
new Run("Some more text"),
new Run("This is some text") { TextDecorations = TextDecorations.Underline }
};
});
}
}
}
}
Your custom control -> BindableTextBlock
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
namespace WpfApplication3
{
public class BindableTextBlock : TextBlock
{
public ICollection<Inline> InlineList
{
get { return (ICollection<Inline>)GetValue(InlineListProperty); }
set { SetValue(InlineListProperty, value); }
}
public static readonly DependencyProperty InlineListProperty =
DependencyProperty.Register("InlineList", typeof(List<Inline>), typeof(BindableTextBlock), new UIPropertyMetadata(null, OnPropertyChanged));
private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue == null) return;
var textBlock = (BindableTextBlock)sender;
textBlock.Inlines.AddRange((ICollection<Inline>)e.NewValue);
}
}
}
Your XAML
On your Page or Window (depending on platform)
DataContext="{Binding Main, Source={StaticResource Locator}}"
Then inside
<StackPanel>
<Button Command="{Binding SendClicked}">SendClicked</Button>
<local:BindableTextBlock Background="Black" Foreground="AliceBlue"
Width = "Auto" Height="Auto"
InlineList="{Binding InlineList}"
>
</local:BindableTextBlock>
</StackPanel>
All assuming you have your ViewModelLocator from MVVM Light register your view model
using GalaSoft.MvvmLight.Ioc;
using Microsoft.Practices.ServiceLocation;
namespace WpfApplication3.ViewModel
{
public class ViewModelLocator
{
public ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
SimpleIoc.Default.Register<MainViewModel>();
}
public MainViewModel Main
{
get
{
return ServiceLocator.Current.GetInstance<MainViewModel>();
}
}
public static void Cleanup()
{
}
}
}
ALTERNATIVE
Alternatively you could have your command like this:
public RelayCommand SendClicked
{
get
{
return new RelayCommand(() =>
{
_inlineList = new List<Inline>();
InlineList.Add(new Run("This is some bold text") { FontWeight = FontWeights.Bold });
InlineList.Add(new Run("Some more text"));
InlineList.Add(new Run("This is some text") { TextDecorations = TextDecorations.Underline });
RaisePropertyChanged("InlineList");
});
}
}
But you have to use the other option of defining the property as described at the beginning of my post.
You could do it in other ways of course.
Just one more advice -> It is considered bad practice and not in the spirit of MVVM to have UI elements in your view model, so change in architecture is strongly advised in this code IMO.
Post got too long (as usual), if you need aditional explanation please let me know.
Cheers and happy coding ;)

WPF/C# entirely programmatically binding an array of objects to a static ObservableCollection

Please assume this entire question deals in code, without any XAML.
I have a static ObservableCollection named myStaticList. It's a part of a non-static class named myClass.
public class myClass
{
public static ObservableCollection<CheckBoxStructure> myStaticList { get; set; }
static myClass()
{
myStaticList = new ObservableCollection<CheckBoxStructure>();
}
}
And the definition of CheckBoxStructure:
public class CheckBoxStructure
{
public string Description { get; set; }
public bool IsSelected { get; set; }
}
In addition, there's an array of checkboxes called checkBoxArray[], holding 3 elements. each checkbox has as content a textbox.
What I want to do is programmatically bind (two-way) these two, in such a manner that the IsChecked property of the checkboxes in the checkBoxArray[] array will bind to the IsSelected property of the myStaticList's CheckBoxStructure, and similarly so between the text of the textboxes inthe checkboxes' content and the Description property of the myStaticList's CheckBoxStructure.
In addition, I would like to avoid using loops, since it is preferable that this two lists will update each other if they change in size.
How is this possible?
Thanks!
Using XAML, an easy way would be to the declare an ItemsControl and a DataTemplate for it so that you can have a UserControl (CheckBox and TextBox inside) with its DataContext being a CheckBoxStructure. This way the bindings work between CheckBox.IsChecked and IsSelected property and between TextBox.Text and Description property.
If you need to this only in code then you would have to create same behavior (ItemsControl with a DataTemplate). You have at least 2 options
1.
DataTemplate template = new DataTemplate();
FrameworkElementFactory factory = new FrameworkElementFactory(typeof(StackPanel));
template.VisualTree = factory;
FrameworkElementFactory childFactory = new FrameworkElementFactory(typeof(CheckBox));
childFactory.SetBinding(CheckBox.IsChecked, new Binding("IsSelected"));
factory.AppendChild(childFactory);
childFactory = new FrameworkElementFactory(typeof(TextBox));
childFactory.SetBinding(Label.ContentProperty, new Binding("Description"));
factory.AppendChild(childFactory);
2.
MemoryStream sr = null;
ParserContext pc = null;
string xaml = string.Empty;
xaml = "<DataTemplate><StackPanel><TextBlock Text="{Binding Description"/><CheckBox IsChecked="{Binding IsSelected"/></StackPanel></DataTemplate>";
sr = new MemoryStream(Encoding.ASCII.GetBytes(xaml));
pc = new ParserContext();
pc.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
pc.XmlnsDictionary.Add("x", "http://schemas.microsoft.com/winfx/2006/xaml");
DataTemplate datatemplate = (DataTemplate)XamlReader.Load(sr, pc);
this.Resources.Add("dt", datatemplate);
Later edit, after discussion from comments; this example works only one way of binding but is easily to make it two ways. Please note that this is only a trivial example of a concept and is not complete: you need to modify the list classes to suit how you wish for objects to be paired, you may need to add more guards for corner cases, you may need to make it thread safe and so on...
First the basic binding objects:
class Binder
{
public Binder()
{
_bindings = new Dictionary<string, List<string>>();
}
private INotifyPropertyChanged _dataContext;
public INotifyPropertyChanged DataContext
{
get { return _dataContext; }
set
{
if (_dataContext != null)
{
_dataContext.PropertyChanged -= _dataContext_PropertyChanged;
}
_dataContext = value;
_dataContext.PropertyChanged += _dataContext_PropertyChanged;
}
}
void _dataContext_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (_bindings.ContainsKey(e.PropertyName))
{
var bindableType = _dataContext.GetType();
var bindableProp = bindableType.GetProperty(e.PropertyName);
if (bindableProp == null)
{
return;
}
var binderType = this.GetType();
foreach (var binderPropName in _bindings[e.PropertyName])
{
var binderProp = binderType.GetProperty(binderPropName);
if (binderProp == null)
{
continue;
}
var value = bindableProp.GetValue(_dataContext);
binderProp.SetValue(this, value);
}
}
}
Dictionary<string, List<string>> _bindings;
public void AddBinding(string binderPropertyName, string bindablePropertyName)
{
if (!_bindings.ContainsKey(bindablePropertyName))
{
_bindings.Add(bindablePropertyName, new List<string>());
}
_bindings[bindablePropertyName].Add(bindablePropertyName);
}
}
class Bindable : INotifyPropertyChanged
{
protected void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
Then the holding lists for them:
class BindableList<T> : List<T> where T : Bindable
{
public event Action<T> ItemAdded;
public new void Add(T item)
{
base.Add(item);
NotifyItemAdded(item);
}
private void NotifyItemAdded(T item)
{
if (ItemAdded != null)
{
ItemAdded(item);
}
}
}
class BinderList<T> : List<T> where T : Binder
{
public BinderList()
{
_bindingRules = new Dictionary<string, string>();
}
private BindableList<Bindable> _dataContextList;
public BindableList<Bindable> DataContextList
{
get { return _dataContextList; }
set
{
if (_dataContextList != null)
{
_dataContextList.ItemAdded -= _dataContextList_ItemAdded;
}
_dataContextList = value;
_dataContextList.ItemAdded += _dataContextList_ItemAdded;
}
}
void _dataContextList_ItemAdded(Bindable obj)
{
foreach (var pair in _bindingRules)
{
this[Count-1].AddBinding(pair.Key, pair.Value);
this[Count - 1].DataContext = obj;
}
}
private Dictionary<string, string> _bindingRules;
public void AddBindingRule(string binderPropertyName, string bindablePropertyName)
{
_bindingRules.Add(binderPropertyName, bindablePropertyName);
}
}
Now the actual classes with properties:
class BinderElement : Binder
{
private string _description;
public string Description
{
get { return _description; }
set { _description = value; }
}
}
class BindableElement : Bindable
{
private string _description;
public string Description
{
get
{
return _description;
}
set
{
_description = value;
NotifyPropertyChanged("Description");
}
}
}
And an example to use them:
static void Main(string[] args)
{
var bindableList = new BindableList<Bindable>();
var binderList = new BinderList<BinderElement>()
{
new BinderElement(),
new BinderElement()
};
binderList.DataContextList = bindableList;
binderList.AddBindingRule("Description", "Description");
bindableList.Add(new BindableElement());
bindableList.Add(new BindableElement());
((BindableElement)bindableList[1]).Description = "This should arrive in BinderElement Description property";
Console.WriteLine(binderList[1].Description);
Console.ReadLine();
}

WPF: Datagrid - Apply DataGridTemplateColumn.CellTemplate dynamically

I am quite new to WPF (from Winforms). I am using .Net 4.5 and the default DataGrid that comes along with the framework in WPF. The columns are created dynamically because I do not know at compile time. Now, based on data some columns will be read-only and some will be of ComboBox type.
How can I apply this logic dynamically while creating the columns dynamically as shown below. here is the code which I wrote so far. Whenever the data changes, the columns are generated dynamically based on the data.
Also, how do I generate "different types" of column dynamically (ComboBox, TextBox, etc...) based on data. The MVVM-ish way in WPF is kind of restricting me because I do not have much knowledge about templating. I am sure it should be easy once I get through.
NB: Currently all this is working fine. I have a read-only databound grid. But, there is no support for selective editable columns and selective ComboBox columns.
public class DatagridExtension {
public static readonly DependencyProperty RefDataSourceProperty =
DependencyProperty.RegisterAttached(
"RefDataSource",
typeof(RefDataRecord),
typeof(DatagridExtension),
new PropertyMetadata( default(RefDataRecord), OnRefDataSourceChanged)
);
private static void OnRefDataSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var grid = d as DataGrid;
var dataSource = e.NewValue as RefDataRecord;
grid.ItemsSource = dataSource;
grid.Columns.Clear();
int count = 0;
foreach (var col in dataSource.Columns)
{
grid.Columns.Add(
new DataGridTextColumn
{
Header = col.Name,
Binding = new Binding(string.Format("[{0}]", count))
}
);
count++;
}
}
public static RefDataRecord GetRefDataSource(DependencyObject dependencyObject)
{
return (RefDataRecord) dependencyObject.GetValue(RefDataSourceProperty);
}
public static void SetRefDataSource(DependencyObject dependencyObject, RefDataRecord value)
{
dependencyObject.SetValue(RefDataSourceProperty, value);
}
}
http://msdn.microsoft.com/en-us/library/system.windows.controls.datagridtemplatecolumn.celltemplate(v=vs.95).aspx
WPF DataGrid creates DataGridComboBoxColumn by default if data source property type derives from Enum and sets DataGridColumn.IsReadyOnly by default if property doesn't have public setter or if property has ReadOnlyAttribute with ReadOnlyAttribute.IsReadOnly = true.
I will now show how to customize DataGrid column generation if your data source properties do not satisfy default conditions stated above.
Firstly, I will introduce two attributes used to specify that property is read-only (EditableAttribute) and that property should be visualized as ComboBox with predefined drop-down items (NameValueAttribute).
Here is EditableAttribute.cs:
using System;
namespace WpfApplication
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class EditableAttribute : Attribute
{
public bool AllowEdit { get; set; }
}
}
Here is NameValueAttribute.cs:
using System;
namespace WpfApplication
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public sealed class NameValueAttribute : Attribute
{
public string Name { get; set; }
public object Value { get; set; }
}
}
Next, we need some sample classes that will be used for demonstration.
So here is Person.cs class that will represent a single item (row) in a DataGrid:
using System.ComponentModel;
namespace WpfApplication
{
public class Person : ObservableObject
{
private string name;
private string surname;
private char gender;
public string Name
{
get { return this.name; }
set { this.SetValue(ref this.name, value, "Name"); }
}
[Editable(AllowEdit = false)]
public string Surname
{
get { return this.surname; }
set { this.SetValue(ref this.surname, value, "Surname"); }
}
[NameValue(Name = "Male", Value = 'M')]
[NameValue(Name = "Female", Value = 'F')]
public char Gender
{
get { return this.gender; }
set { this.SetValue(ref this.gender, value, "Gender"); }
}
}
}
Notice how Surname property has EditableAttribute applied and Gender property has NameValueAttributes applied.
And here is People.cs class that will represent DataGrid's data source:
using System.Collections.ObjectModel;
namespace WpfApplication
{
public class People : ObservableCollection<Person>
{
public People()
{
for (int i = 0; i < 100; ++i)
this.Items.Add(new Person()
{
Name = "Name " + i,
Surname = "Surname " + i,
Gender = i % 2 == 0 ? 'M' : 'F'
});
}
}
}
Base class for Person is ObservableObject.cs which is common to all data-binding applications:
using System.Collections.Generic;
using System.ComponentModel;
namespace WpfApplication
{
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
var handler = this.PropertyChanged;
if (handler != null)
handler(this, e);
}
protected void SetValue<T>(ref T field, T value, string propertyName)
{
if (!EqualityComparer<T>.Default.Equals(field, value))
{
field = value;
this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
}
}
}
Now, here is a XAML for MainWindow.xaml that hosts DataGrid control:
<Window x:Class="WpfApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication">
<Window.Resources>
<local:People x:Key="itemsSource"/>
</Window.Resources>
<DataGrid ItemsSource="{StaticResource itemsSource}" AutoGeneratingColumn="OnAutoGeneratingColumn"/>
</Window>
Crucial part is DataGrid.AutoGeneratingColumn event handler OnAutoGeneratingColumn.
This event gets fired after DataGrid generates a DataGridColumn and is fired once for every auto-generated column. It is used to customize the auto-generated column or specify different one, depending on the provided data source property.
Here is MainWindow.xaml.cs code-behind in which OnAutoGeneratingColumn event handler does exactly that. It customized generated column by setting it as read-only if data source property has EditableAttribute with AllowEdit = false, and it overrides auto-generated column with DataGridComboBoxColumn if data source property has NameValueAttributes:
using System;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace WpfApplication
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void OnAutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
var propertyDescriptor = (PropertyDescriptor)e.PropertyDescriptor;
var dataBoundColumn = (DataGridBoundColumn)e.Column;
var comboBoxColumn = GenerateComboBoxColumn(propertyDescriptor, dataBoundColumn);
if (comboBoxColumn != null)
e.Column = comboBoxColumn;
if (IsReadOnlyProperty(propertyDescriptor))
e.Column.IsReadOnly = true;
}
private static DataGridComboBoxColumn GenerateComboBoxColumn(PropertyDescriptor propertyDescriptor, DataGridBoundColumn dataBoundColumn)
{
var nameValueAttributes = Attribute.GetCustomAttributes(propertyDescriptor.ComponentType.GetProperty(propertyDescriptor.Name)).OfType<NameValueAttribute>().ToArray();
if (nameValueAttributes.Length > 0)
return new DataGridComboBoxColumn()
{
ItemsSource = nameValueAttributes,
DisplayMemberPath = "Name",
SelectedValuePath = "Value",
SelectedValueBinding = dataBoundColumn.Binding
};
else
return null;
}
private static bool IsReadOnlyProperty(PropertyDescriptor propertyDescriptor)
{
var editableAttribute = propertyDescriptor.Attributes.OfType<EditableAttribute>().FirstOrDefault();
return editableAttribute != null ? !editableAttribute.AllowEdit : false;
}
}
}
UPDATE FOR DYNAMIC CASE:
WPF supports dynamic reflection with ICustomTypeDescriptor implemented on data items and ITypedList implemented on collection.
Also, .NET 4.5 supports ICustomTypeProvider, but since I do not have .NET 4.5 installed, I haven't tested it.
NameValueAttribute.cs is same as before.
Here is very simple implementation of ICustomTypeDescriptor and ITypedList in a working sample:
DataProperty.cs
using System;
using System.ComponentModel;
namespace WpfApplication
{
public class DataProperty : PropertyDescriptor
{
private readonly Type propertyType;
private readonly bool isReadOnly;
private readonly Attribute[] attributes;
public DataProperty(string propertyName, Type propertyType, bool isReadOnly, params Attribute[] attributes)
: base(propertyName, null)
{
this.propertyType = propertyType;
this.isReadOnly = isReadOnly;
this.attributes = attributes;
}
protected override Attribute[] AttributeArray
{
get { return this.attributes; }
set { throw new NotImplementedException(); }
}
public override Type ComponentType
{
get { return typeof(DataRecord); }
}
public override Type PropertyType
{
get { return this.propertyType; }
}
public override bool IsReadOnly
{
get { return this.isReadOnly; }
}
public override object GetValue(object component)
{
return ((DataRecord)component)[this.Name];
}
public override void SetValue(object component, object value)
{
if (!this.isReadOnly)
((DataRecord)component)[this.Name] = value;
}
#region Not implemented PropertyDescriptor Members
public override bool CanResetValue(object component)
{
throw new NotImplementedException();
}
public override void ResetValue(object component)
{
throw new NotImplementedException();
}
public override bool ShouldSerializeValue(object component)
{
throw new NotImplementedException();
}
#endregion
}
}
DataRecord.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
namespace WpfApplication
{
public class DataRecord : INotifyPropertyChanged, ICustomTypeDescriptor
{
public event PropertyChangedEventHandler PropertyChanged;
internal ITypedList container;
private readonly IDictionary<string, object> values = new SortedList<string, object>();
public object this[string propertyName]
{
get
{
object value;
this.values.TryGetValue(propertyName, out value);
return value;
}
set
{
if (!object.Equals(this[propertyName], value))
{
this.values[propertyName] = value;
this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
}
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
var handler = this.PropertyChanged;
if (handler != null)
handler(this, e);
}
PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties()
{
return this.container.GetItemProperties(null);
}
#region Not implemented ICustomTypeDescriptor Members
AttributeCollection ICustomTypeDescriptor.GetAttributes()
{
throw new NotImplementedException();
}
string ICustomTypeDescriptor.GetClassName()
{
throw new NotImplementedException();
}
string ICustomTypeDescriptor.GetComponentName()
{
throw new NotImplementedException();
}
TypeConverter ICustomTypeDescriptor.GetConverter()
{
throw new NotImplementedException();
}
EventDescriptor ICustomTypeDescriptor.GetDefaultEvent()
{
throw new NotImplementedException();
}
PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty()
{
throw new NotImplementedException();
}
object ICustomTypeDescriptor.GetEditor(Type editorBaseType)
{
throw new NotImplementedException();
}
EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes)
{
throw new NotImplementedException();
}
EventDescriptorCollection ICustomTypeDescriptor.GetEvents()
{
throw new NotImplementedException();
}
PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[] attributes)
{
throw new NotImplementedException();
}
object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd)
{
throw new NotImplementedException();
}
#endregion
}
}
DataRecordCollection.cs:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace WpfApplication
{
public class DataRecordCollection<T> : ObservableCollection<T>, ITypedList where T : DataRecord
{
private readonly PropertyDescriptorCollection properties;
public DataRecordCollection(params DataProperty[] properties)
{
this.properties = new PropertyDescriptorCollection(properties);
}
protected override void InsertItem(int index, T item)
{
item.container = this;
base.InsertItem(index, item);
}
PropertyDescriptorCollection ITypedList.GetItemProperties(PropertyDescriptor[] listAccessors)
{
return this.properties;
}
string ITypedList.GetListName(PropertyDescriptor[] listAccessors)
{
throw new NotImplementedException();
}
}
}
MainWindow.xaml:
<Window x:Class="WpfApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication">
<DataGrid x:Name="dataGrid" AutoGeneratingColumn="OnAutoGeneratingColumn"/>
</Window>
MainWindow.xaml.cs:
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace WpfApplication
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var records = new DataRecordCollection<DataRecord>(
new DataProperty("Name", typeof(string), false),
new DataProperty("Surname", typeof(string), true),
new DataProperty("Gender", typeof(char), false, new NameValueAttribute() { Name = "Male", Value = 'M' }, new NameValueAttribute() { Name = "Female", Value = 'F' }));
for (int i = 0; i < 100; ++i)
{
var record = new DataRecord();
record["Name"] = "Name " + i;
record["Surname"] = "Surname " + i;
record["Gender"] = i % 2 == 0 ? 'M' : 'F';
records.Add(record);
}
this.dataGrid.ItemsSource = records;
}
private void OnAutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
e.Column.Header = ((PropertyDescriptor)e.PropertyDescriptor).DisplayName;
var propertyDescriptor = (PropertyDescriptor)e.PropertyDescriptor;
var dataBoundColumn = (DataGridBoundColumn)e.Column;
var comboBoxColumn = GenerateComboBoxColumn(propertyDescriptor, dataBoundColumn);
if (comboBoxColumn != null)
e.Column = comboBoxColumn;
}
private static DataGridComboBoxColumn GenerateComboBoxColumn(PropertyDescriptor propertyDescriptor, DataGridBoundColumn dataBoundColumn)
{
var nameValueAttributes = propertyDescriptor.Attributes.OfType<NameValueAttribute>().ToArray();
if (nameValueAttributes.Length > 0)
return new DataGridComboBoxColumn()
{
ItemsSource = nameValueAttributes,
DisplayMemberPath = "Name",
SelectedValuePath = "Value",
SelectedValueBinding = dataBoundColumn.Binding
};
else
return null;
}
}
}
Firstly, one of the main advantages of WPF to WinForms is ability to declare user interface using templates. And you should avoid declaring UI components in code as as possible.
As i understand you want to display collection of different objects based on object type/data.
The best way to implement such logic - implement your own TemplateSelector
I suggest you read next articles:
http://www.wpftutorial.net/DataGrid.html
http://www.switchonthecode.com/tutorials/wpf-tutorial-how-to-use-a-datatemplateselector
P.S.
For reference. Example of declaring DataTemplate in code:
//create the data template
DataTemplate cardLayout = new DataTemplate();
cardLayout.DataType = typeof(CreditCardPayment);
//set up the stack panel
FrameworkElementFactory spFactory = new FrameworkElementFactory(typeof(StackPanel));
spFactory.Name = "myComboFactory";
spFactory.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal);
//set up the card holder textblock
FrameworkElementFactory cardHolder = new FrameworkElementFactory(typeof(TextBlock));
cardHolder.SetBinding(TextBlock.TextProperty, new Binding("BillToName"));
cardHolder.SetValue(TextBlock.ToolTipProperty, "Card Holder Name");
spFactory.AppendChild(cardHolder);
//set up the card number textblock
FrameworkElementFactory cardNumber = new FrameworkElementFactory(typeof(TextBlock));
cardNumber.SetBinding(TextBlock.TextProperty, new Binding("SafeNumber"));
cardNumber.SetValue(TextBlock.ToolTipProperty, "Credit Card Number");
spFactory.AppendChild(cardNumber);
//set up the notes textblock
FrameworkElementFactory notes = new FrameworkElementFactory(typeof(TextBlock));
notes.SetBinding(TextBlock.TextProperty, new Binding("Notes"));
notes.SetValue(TextBlock.ToolTipProperty, "Notes");
spFactory.AppendChild(notes);
//set the visual tree of the data template
cardLayout.VisualTree = spFactory;
//set the item template to be our shiny new data template
drpCreditCardNumberWpf.ItemTemplate = cardLayout;
but as i say above, you should avoid this.
This is the correct answer - http://www.paulstovell.com/dynamic-datagrid (see the template creation logic dynamically. Its clever).
And, MMVM will be achieved like this - http://www.codeproject.com/Articles/36462/Binding-a-ListView-to-a-Data-Matrix (almost what I have posted in the question)
I was away from the Internet for a few days, but I think that I have found the better approach with simplified PropertyDescriptor architecture which doesn't require to implement ICustomTypeDescriptor. Here is the entire code:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace WpfApplication
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var records = new RecordCollection(new Property("Name"), new Property("Surname"));
for (int i = 0; i < 1000; ++i)
records.Add(new Record()
{
{ "Name", "John " + i },
{ "Surname", "Doe " + i }
});
this.dataGrid.ItemsSource = records;
}
private void OnAutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
var property = e.PropertyDescriptor as Property;
if (property != null)
{
var binding = new Binding() { Path = new PropertyPath(property), Mode = property.IsReadOnly ? BindingMode.OneWay : BindingMode.TwoWay };
var dataGridBoundColumn = e.Column as DataGridBoundColumn;
if (dataGridBoundColumn != null)
dataGridBoundColumn.Binding = binding;
else
{
var dataGridComboBoxColumn = e.Column as DataGridComboBoxColumn;
if (dataGridComboBoxColumn != null)
dataGridComboBoxColumn.SelectedItemBinding = binding;
}
}
}
}
public sealed class Record : INotifyPropertyChanged, IEnumerable
{
public event PropertyChangedEventHandler PropertyChanged;
private readonly IDictionary<string, object> values = new SortedList<string, object>(StringComparer.Ordinal);
private void OnPropertyChanged(PropertyChangedEventArgs e)
{
var handler = this.PropertyChanged;
if (handler != null)
handler(this, e);
}
public object GetValue(string name)
{
object value;
return this.values.TryGetValue(name, out value) ? value : null;
}
public void SetValue(string name, object value)
{
if (!object.Equals(this.GetValue(name), value))
{
this.values[name] = value;
this.OnPropertyChanged(new PropertyChangedEventArgs(name));
}
}
public void Add(string name, object value)
{
this.values[name] = value;
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.values.GetEnumerator();
}
}
public sealed class Property : PropertyDescriptor
{
private readonly Type propertyType;
private readonly bool isReadOnly;
public Property(string name)
: this(name, typeof(string))
{
}
public Property(string name, Type propertyType)
: this(name, propertyType, false)
{
}
public Property(string name, Type propertyType, bool isReadOnly, params Attribute[] attributes)
: base(name, attributes)
{
this.propertyType = propertyType;
this.isReadOnly = isReadOnly;
}
public override Type ComponentType
{
get { return typeof(Record); }
}
public override Type PropertyType
{
get { return this.propertyType; }
}
public override bool IsReadOnly
{
get { return this.isReadOnly; }
}
public override object GetValue(object component)
{
var record = component as Record;
return record != null ? record.GetValue(this.Name) : null;
}
public override void SetValue(object component, object value)
{
var record = component as Record;
if (record != null)
record.SetValue(this.Name, value);
}
public override bool CanResetValue(object component)
{
throw new NotSupportedException();
}
public override void ResetValue(object component)
{
throw new NotSupportedException();
}
public override bool ShouldSerializeValue(object component)
{
throw new NotSupportedException();
}
}
public sealed class RecordCollection : ObservableCollection<Record>, ITypedList
{
private readonly PropertyDescriptorCollection properties;
public RecordCollection(params Property[] properties)
{
this.properties = new PropertyDescriptorCollection(properties);
}
PropertyDescriptorCollection ITypedList.GetItemProperties(PropertyDescriptor[] listAccessors)
{
return this.properties;
}
string ITypedList.GetListName(PropertyDescriptor[] listAccessors)
{
return string.Empty;
}
}
}
<Window x:Class="WpfApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication">
<DataGrid x:Name="dataGrid" AutoGeneratingColumn="OnAutoGeneratingColumn"/>
</Window>
The key thing in this code is creating a Binding with a BindingPath that contains a Property instance, instead of a string. This enables a simplification of PropertyDescriptor architecture because ICustomTypeDescriptor is not required anymore.
What do you think about this solution?

Editing newly added row in Silverlight 3 DataGrid using MVVM

I am trying to use the Silverlight 3.0 DataGrid with the MVVM design pattern. My page has a DataGrid and a button that adds an item to the collection in the VM using a command (from the Composite Application Library). This works fine, and the new item is displayed and selected.
The problem I can't solve is how to begin editing the row. I want the new row to be immediately editable when the user clicks the Add button i.e. focus set to the DataGrid and the new row in edit mode.
This is the XAML in the view:
<Grid x:Name="LayoutRoot">
<StackPanel>
<data:DataGrid ItemsSource="{Binding DataView}"/>
<Button cmd:Click.Command="{Binding AddItemCommand}" Content="Add" />
</StackPanel>
</Grid>
The code behind has one line of code that creates an instance of the VM and sets the DataContext of the view.
The VM code is:
public class VM
{
public List<TestData> UnderlyingData { get; set; }
public PagedCollectionView DataView { get; set; }
public ICommand AddItemCommand { get; set; }
public VM()
{
AddItemCommand = new DelegateCommand<object>(o =>
{
DataView.AddNew();
});
UnderlyingData = new List<TestData>();
UnderlyingData.Add(new TestData() { Value = "Test" });
DataView = new PagedCollectionView(UnderlyingData);
}
}
public class TestData
{
public string Value { get; set; }
public TestData()
{
Value = "<new>";
}
public override string ToString()
{
return Value.ToString();
}
}
What would be the best way to solve this problem using the MVVM design pattern?
I faced the same issue. I've introduced interface ISupportEditingState:
public interface ISupportEditingState
{
EditingState EditingState { get; set; }
}
My VM implements it. And then I wrote this behaviour to synchronise editing state of DataGrid and my VM:
public class SynchroniseDataGridEditingStateBehaviour : Behavior<DataGrid>
{
public static readonly DependencyProperty EditingStateBindingProperty =
DependencyProperty.Register("EditingStateBinding", typeof(ISupportEditingState),
typeof(SynchroniseDataGridEditingStateBehaviour), new PropertyMetadata(OnEditingStateBindingPropertyChange));
private bool _attached;
private bool _changingEditingState;
public ISupportEditingState EditingStateBinding
{
get { return (ISupportEditingState)GetValue(EditingStateBindingProperty); }
set { SetValue(EditingStateBindingProperty, value); }
}
private static void OnEditingStateBindingPropertyChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var b = d as SynchroniseDataGridEditingStateBehaviour;
if (b == null)
return;
var oldNotifyChanged = e.OldValue as INotifyPropertyChanged;
if (oldNotifyChanged != null)
oldNotifyChanged.PropertyChanged -= b.OnEditingStatePropertyChanged;
var newNotifyChanged = e.NewValue as INotifyPropertyChanged;
if (newNotifyChanged != null)
newNotifyChanged.PropertyChanged += b.OnEditingStatePropertyChanged;
var newEditingStateSource = e.NewValue as ISupportEditingState;
if (newEditingStateSource.EditingState == EditingState.Editing)
{
// todo: mh: decide on this behaviour once again.
// maybe it's better to start editing if selected item is already bound in the DataGrid
newEditingStateSource.EditingState = EditingState.LastCancelled;
}
}
private static readonly string EditingStatePropertyName =
CodeUtils.GetPropertyNameByLambda<ISupportEditingState>(ses => ses.EditingState);
private void OnEditingStatePropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (_changingEditingState || !_attached || e.PropertyName != EditingStatePropertyName)
return;
_changingEditingState = true;
var editingStateSource = sender as ISupportEditingState;
if (editingStateSource == null)
return;
var grid = AssociatedObject;
var editingState = editingStateSource.EditingState;
switch (editingState)
{
case EditingState.Editing:
grid.BeginEdit();
break;
case EditingState.LastCancelled:
grid.CancelEdit();
break;
case EditingState.LastCommitted:
grid.CommitEdit();
break;
default:
throw new InvalidOperationException("Provided EditingState is not supported by the behaviour.");
}
_changingEditingState = false;
}
protected override void OnAttached()
{
var grid = AssociatedObject;
grid.BeginningEdit += OnBeginningEdit;
grid.RowEditEnded += OnEditEnded;
_attached = true;
}
protected override void OnDetaching()
{
var grid = AssociatedObject;
grid.BeginningEdit -= OnBeginningEdit;
grid.RowEditEnded -= OnEditEnded;
_attached = false;
}
void OnEditEnded(object sender, DataGridRowEditEndedEventArgs e)
{
if (_changingEditingState)
return;
EditingState editingState;
if (e.EditAction == DataGridEditAction.Commit)
editingState = EditingState.LastCommitted;
else if (e.EditAction == DataGridEditAction.Cancel)
editingState = EditingState.LastCancelled;
else
return; // if DataGridEditAction will ever be extended, this part must be changed
EditingStateBinding.EditingState = editingState;
}
void OnBeginningEdit(object sender, DataGridBeginningEditEventArgs e)
{
if (_changingEditingState)
return;
EditingStateBinding.EditingState = EditingState.Editing;
}
}
Works ok for me, hope it helps.
Whenever you talk about directly accessing ui components, your kinda missing the point of mvvm. The ui binds to the viewmodel, so find a way to alter the viewmodel instead.

Categories

Resources