MVVM command binding of nested view models - wpf

I am using the view models in the class diagram below for a time sheet presentation using a DataGrid.
The top class (ActivityCollectionViewModel) is the DataContext for the grid; the collection of activities (ActivityViewModel) it holds are line items in the grid. The activity has a collection of allocations (AllocationViewModel) which are the majority of the line item DataGrid cells (columns).
Please notice that the AllocationVm (cell) has it's own command, the MakeFullDayCommand. In the current design I have equivalent commands in both the AllocationVm's parent as well as its grandparent. I did it this way thinking I could bind the grandparent's command and then use the collectionViewSource's ability to maintain the selected chiild vms so that the correct cell's command would always be the one invoked.
In practice, it is confusing to track and I am having trouble getting binding alone to keep everything synchronized, so I have resorted to several code behind hacks in the DataGrid, as shown below.
So I thought I'd step back and see if maybe someone could suggest a simpler more effective design than what I've got, or confirm that this is a viable solution and help me get a better data binding strategy in place.
Cheers,
Berryl
How it works
The bottom command in the context menu below is that nested command I am referring to.
Code Behind
This code is butt ugly and tough to test!
/// <summary>
/// Synchronize the <see cref="ActivityViewModel.SelectedAllocationVm"/> here so the input binding
/// key (F8) is always working on the correct command.
/// </summary>
private void OnCurrentCellChanged(object sender, EventArgs e)
{
if (sender == null) return;
var grid = (DataGrid)sender;
if (grid.CurrentColumn == null) return;
var selectedActivity = (ActivityViewModel)grid.CurrentItem;
if (_isEditableDayOfTheWeekColumn(grid.CurrentColumn))
{
var dowCol = (DayOfTheWeekColumn)grid.CurrentColumn;
var index = Convert.ToInt32(dowCol.DowIndex);
selectedActivity.SetSelectedAllocationVm(index);
}
else
{
selectedActivity.SetSelectedAllocationVm(-1);
}
}
/// <summary>
/// Invoke the MakeFullDayCommand when the user double clicks an editable cell;
/// synchronize the selected allocation view model first.
/// </summary>
private void OnDoubleClick(object sender, MouseButtonEventArgs e)
{
if (sender == null) return;
var grid = (DataGrid)sender;
if (grid.CurrentColumn == null) return;
if (!_isEditableDayOfTheWeekColumn(grid.CurrentColumn)) return;
var selectedActivity = (ActivityViewModel) grid.CurrentItem;
var dowCol = (DayOfTheWeekColumn)grid.CurrentColumn;
var index = Convert.ToInt32(dowCol.DowIndex);
var allocationVm = selectedActivity.SetSelectedAllocationVm(index);
if (allocationVm.MakeFullDayCommand.CanExecute(null))
{
allocationVm.MakeFullDayCommand.Execute(null);
}
}
/// <summary>
/// Manipululate the context menu to show the correct description of the MakeFullDayCommand.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="System.Windows.Controls.ContextMenuEventArgs"/> instance containing the event data.</param>
void OnContextMenuOpening(object sender, ContextMenuEventArgs e) {
if (sender == null) return;
var grid = (DataGrid)sender;
if (grid.CurrentColumn == null) return;
const int INDEX_OF_MAKE_FULL_DAY_CMD = 1;
if (_isEditableDayOfTheWeekColumn(grid.CurrentColumn)) {
var selectedActivity = (ActivityViewModel) grid.CurrentItem;
var dowCol = (DayOfTheWeekColumn) grid.CurrentColumn;
var index = Convert.ToInt32(dowCol.DowIndex);
var allocationVm = selectedActivity.SetSelectedAllocationVm(index);
var menuItem = allocationVm.MakeFullDayCommand.ToMenuItem();
if (grid.ContextMenu.Items.Count == 1) {
Log.Info("{0}", allocationVm.MakeFullDayCommand.HeaderText);
grid.ContextMenu.Items.Add(menuItem);
}
else {
var currentItem = (MenuItem) grid.ContextMenu.Items.GetItemAt(INDEX_OF_MAKE_FULL_DAY_CMD);
if (currentItem.Command != menuItem.Command) {
// remove the outdated menu item before adding back the new one
grid.ContextMenu.Items.Remove(currentItem);
grid.ContextMenu.Items.Add(menuItem);
}
}
}
else
{
if (grid.ContextMenu.Items.Count == 2)
{
// we aren't on an editable cell - remove the command altogether
grid.ContextMenu.Items.RemoveAt(INDEX_OF_MAKE_FULL_DAY_CMD);
}
}
}

In my experience with the data grid (and what seems like yours), I have had a hard time trying to get it to bind to columns by way of nested view models. Last time I tried to use it, I ended up downloading the source of the data grid and rewriting a bunch of it to support binding the way I needed. If I could start over, I would just write my own from scratch with my limited functionality.
Besides that, it may be beneficial to look at a different way of displaying your data to the end user that may work a little nicer in both user experience and coding and testability. Seems like it will be hard for a user to look at the grid and think "I should right click on the column to make a full day."
Also, part of the goodness of WPF is the ability to make a control REALLY easily. Maybe that might be a better route for you?

Related

wpf mvvm : Why duplicating model collections in view models when binding to a treeview? how to avoid it?

I have a 2-levels hierarchy in my model composed of constellations and entities, hosted in a root object, and I want to show them in a TreeView.
Root object
L Constellations
L Entities
I have my RootObjectViewModel exposing my root object. In that, I fully agree with Bryan Lagunas in MVVM best practices, that the model object should be exposed by the view model instead of doing facade. Excellent video by the way, really worth the time.
I read everywhere that the TreeView.ItemsSource should be mapped to a collection of viewmodels, that is, for my collection of Constellation, my RootObjectViewModel should provide a collection of ConstellationViewModel.
My concern is that if my collections of constellations, and entities within, are live, that is, if some items are added, changed (their order) or removed, I have to manually reflect those changes in my ViewModels' collections.
I would find it more elegant to map the ItemsSource to, say, the collection of Constellation itself, so that any change in the model is reflected without duplication, and then have some converter or so to map the TreeViewItems.DataContext to a ConstellationViewModel based on the constellation model.
If this is not clear enough, I can clarify this with some sample code.
Did anyone face the same question and/or try to implement this ?
Thanks you in advance for your help.
Cedric
It depends. If your model has exactly the properties the view needs, and the view can directly alter them when the user clicks around, it's fine to expose the model.
But if your model is, for example, read only and require calls to a service to apply changes, you have to wrap it in a view model to provide the view with writeable properties.
Got it working !
It is not possible out-of-the-box, and here's why:
It is possible to use model collections as items source, and to use a converter to get the appropriate view model it the components inside the TreeViewItem. But there isn't any way to interfere with the creation of TreeViewItem to apply the converter to its DataContext. Which means that the TreeViewItem's properties can't be binded to the ViewModel.
In other words :
if you want to stick with the standard behavior of the TreeView and don't have to deal with the TreeViewItems properties, if either your collections don't change or can implement ICollectionChanged, and if your models don't change or can implement IPropertyChanged, it is fine to go with the model's collections.
If any of these conditions is broken, then you will have to go with building ViewModel's collections and sync them with model's collection.
Now, I implemented a collection type named ConvertingCollection<Tin, Tout> that uses an original collection as an input and syncs its own contents with this input. This is just a basic class with many many ways of improvement, but it works. All you have to do is use this collection as a VM property, set the original collection and converter, and bind the ItemsSource to this collection.
Here's the full code:
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Globalization;
using System.Windows.Data;
namespace TreeViewPOC
{
class ConvertingCollection<Tin, Tout> : ObservableCollection<Tout>
{
private IValueConverter _converter;
private bool _isObservableCollection;
private IEnumerable<Tin> _originalCollection;
private Dictionary<Tin, Tout> _mapping = new Dictionary<Tin, Tout>();
public ConvertingCollection(IValueConverter converter)
{
// save parameters
_converter = converter;
}
public ConvertingCollection(IEnumerable<Tin> originalCollection, IValueConverter converter)
{
// save parameters
_converter = converter;
OriginalCollection = originalCollection;
}
#region Properties
public IEnumerable<Tin> OriginalCollection
{
get
{
return _originalCollection;
}
set
{
if (!value.Equals(_originalCollection))
{
// manage older collection
if (_originalCollection != null && _isObservableCollection)
{
(_originalCollection as ObservableCollection<Tin>).CollectionChanged -= originalCollection_CollectionChanged;
this.Clear();
}
_originalCollection = value;
// setup original collection information.
_isObservableCollection = _originalCollection is INotifyCollectionChanged;
if (_originalCollection != null && _isObservableCollection)
{
(_originalCollection as INotifyCollectionChanged).CollectionChanged += originalCollection_CollectionChanged;
foreach (Tin item in _originalCollection)
{
AddConverted(item);
}
}
}
}
}
#endregion
/// <summary>
/// Indicates the time in milliseconds between two refreshes.
/// </summary>
/// <notes>
/// When the original collection isn't observable, it must be explored to reflect changes in the converted collection.
/// </notes>
// TODO
//public int RefreshRate { get; set; } = 1000;
/// <summary>
/// Flushes the collection.
/// </summary>
public new void Clear()
{
_mapping.Clear();
base.Clear();
}
#region Events management
private void originalCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (Tin item in e.NewItems)
{
AddConverted(item);
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (Tin item in e.OldItems)
{
RemoveConverted(item);
}
break;
}
}
#endregion
#region Helpers
/// <summary>
/// Converts an item and adds it to the collection.
/// </summary>
/// <param name="item">The original item.</param>
private void AddConverted(Tin item)
{
Tout converted = (Tout) _converter.Convert(item, typeof(Tout), null, CultureInfo.CurrentCulture);
_mapping.Add(item, converted);
this.Add(converted);
}
/// <summary>
/// Removes a converted itemfrom the collection based on its original value.
/// </summary>
/// <param name="item">The original item.</param>
private void RemoveConverted(Tin item)
{
this.Remove(_mapping[item]);
_mapping.Remove(item);
}
#endregion
}
}
I created a project on Github named MVVMTreeViewPOC to POC the idea. It is working fine, and it is a good tool to show the limitations.
Cedric.

WPF rendering is too slow

I am experiencing a strange problem trying to use WPF to render a number of polylines (64 polylines about 400-500 vertices in each on a 2300x1024 Canvas). Polylines are updated every 50ms.
For some reason my application UI becomes very sluggish and almost unresponsive to user input.
I am using to following class to avoid updating the point collection while it is displayed:
class DoubleBufferPlot
{
/// <summary>
/// Double-buffered point collection
/// </summary>
private readonly PointCollection[] mLineBuffer =
{
new PointCollection(),
new PointCollection()
};
private int mWorkingBuffer; //index of the workign buffer (buffer being modified)
#region Properties
//Polyline displayed
public Polyline Display { get; private set; }
/// <summary>
/// index operator to access points
/// </summary>
/// <param name="aIndex">index</param>
/// <returns>Point at aIndex</returns>
public Point this[int aIndex]
{
get { return mLineBuffer[mWorkingBuffer][aIndex]; }
set { mLineBuffer[mWorkingBuffer][aIndex] = value; }
}
/// <summary>
/// Number of points in the working buffer
/// </summary>
public int WorkingPointCount
{
get { return mLineBuffer[mWorkingBuffer].Count; }
set
{
SetCollectionSize(mLineBuffer[mWorkingBuffer], value);
}
}
#endregion
public DoubleBufferPlot(int numPoints = 0)
{
Display = new Polyline {Points = mLineBuffer[1]};
if (numPoints > 0)
{
SetCollectionSize(mLineBuffer[0], numPoints);
SetCollectionSize(mLineBuffer[1], numPoints);
}
}
/// <summary>
/// Swap working and display buffer
/// </summary>
public void Swap()
{
Display.Points = mLineBuffer[mWorkingBuffer]; //display workign buffer
mWorkingBuffer = (mWorkingBuffer + 1) & 1; //swap
//adjust buffer size if needed
if (Display.Points.Count != mLineBuffer[mWorkingBuffer].Count)
{
SetCollectionSize(mLineBuffer[mWorkingBuffer], Display.Points.Count);
}
}
private static void SetCollectionSize(IList<Point> collection, int newSize)
{
while (collection.Count > newSize)
{
collection.RemoveAt(collection.Count - 1);
}
while (collection.Count < newSize)
{
collection.Add(new Point());
}
}
}
I update the working buffer offscreen and then call Swap() to have it displayed. All 64 polylines (DoubleBufferPlot.Display) are added to a Canvas as children.
I used Visual Studio Concurrency Analyzer tool to see what's going on and discovered that after each update the main thread spends 46ms performing some WPF-related tasks: System.Widnows.ContextLayoutManager.UpdateLayout() and System.Windows.Media.MediaContex.Render().
I also discovered that there is another thread that's running almost non-stop rendering
wpfgfx_v0400.dll!CPartitionThread::ThreadMain
...
wpfgfx_v0400.dll!CDrawingContext::Render
...
etc.
I read a number of articles on WPF including this: Can WPF render a line path with 300,000 points on it in a performance-sensitive environment?
and also this article http://msdn.microsoft.com/en-us/magazine/dd483292.aspx.
I am (or my company rather) trying to avoid DrawingVisual since the rest of the project uses WPF shapes API.
Any idea why this is so slow? I even tried disabling anti-aliasing (RenderOptions.SetEdgeMode(mCanvas, EdgeMode.Aliased)) but it did not help very much.
Why does layout update takes so long. Anyone who is an expert in WPF internals?
Thank you very much.
After trying different approaches including DrawingVisual it seems that drawing polylines with so many vertices is too inefficient.
I ended up implementing at approach where I draw polylines only when there 1 or fewer vertices per pixel. Otherwise I render manually to a WriteableBitmap object. This is surprisingly much more efficient.
The fastest way I've found to draw frequently updated geometry is to create a DrawingGroup "backingStore", output that backing store during OnRender(), and then update that backingStore when my data needs to update, by using backingStore.Open(). (see code below)
In my tests, this was more efficient than using WriteableBitmap or RenderTargetBitmap.
If your UI is becoming unresponsive, how are you triggering your redraw every 50ms? Is it possible some of the redraw is taking longer than 50ms and backing up the message-pump with redraw messages? One method to avoid this is to shut off your redraw timer during your redraw loop (or make it a one-shot timer), and only enable it at the end. Another method is to do your redraw during a CompositionTarget.Rendering event, which happens right before the WPF redraw.
DrawingGroup backingStore = new DrawingGroup();
protected override void OnRender(DrawingContext drawingContext) {
base.OnRender(drawingContext);
Render(); // put content into our backingStore
drawingContext.DrawDrawing(backingStore);
}
// I can call this anytime, and it'll update my visual drawing
// without ever triggering layout or OnRender()
private void Render() {
var drawingContext = backingStore.Open();
Render(drawingContext);
drawingContext.Close();
}

How to set scroll position from view model with caliburn.micro?

I have a ListBox in my view, bound to a collection that is dynamically growing. I would like the scroll position to follow the last added item (which is appended to the bottom of the list). How can I achieve this with Caliburn.Micro?
An alternative could be to use the event aggregator to publish a message to the view.
Something like:
Aggregator.Publish(ItemAddedMessage<SomeItemType>(itemThatWasJustAdded));
and in the view:
public class SomeView : IHandle<ItemAddedMessage<SomeItemType>>
{
public void Handle(ItemAddedMessage<SomeItemType> message)
{
// Implement view specific behaviour here
}
}
It depends on what your requirements are but at least then the view is responsible for display concerns and you can still test the VM
Also you could just implement the code solely in the view - since it appears to be a view concern (e.g. using the events that listbox provides)
A behaviour would also be useful but maybe one that's a little less coupled to your types - e.g. a generic behaviour SeekAddedItemBehaviour which hooks listbox events to find the last item. Not sure if the listbox exposes the required events, but worth a look
EDIT:
Ok this may work full stop - you should be able to just attach this behaviour to the listbox and it should take care of the rest:
public class ListBoxSeekLastItemBehaviour : System.Windows.Interactivity.Behavior<ListBox>
{
private static readonly DependencyProperty ItemsSourceWatcherProperty = DependencyProperty.Register("ItemsSourceWatcher", typeof(object), typeof(ListBoxSeekLastItemBehaviour), new PropertyMetadata(null, OnItemsSourceWatcherPropertyChanged));
private ListBox _listBox = null;
private static void OnItemsSourceWatcherPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ListBoxSeekLastItemBehaviour source = d as ListBoxSeekLastItemBehaviour;
if (source != null)
source.OnItemsSourceWatcherPropertyChanged();
}
private void OnItemsSourceWatcherPropertyChanged()
{
// The itemssource has changed, check if it raises collection changed notifications
if (_listBox.ItemsSource is INotifyCollectionChanged)
{
// if it does, hook the CollectionChanged event so we can respond to items being added
(_listBox.ItemsSource as INotifyCollectionChanged).CollectionChanged += new NotifyCollectionChangedEventHandler(ListBoxSeekLastItemBehaviour_CollectionChanged);
}
}
void ListBoxSeekLastItemBehaviour_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems.Count > 0)
{
// If an item was added seek it
ScrollIntoView(e.NewItems[0]);
}
}
protected override void OnAttached()
{
base.OnAttached();
// We've been attached - get the associated listbox
var box = this.AssociatedObject as ListBox;
if (box != null)
{
// Hold a ref
_listBox = box;
// Set a binding to watch for property changes
System.Windows.Data.Binding binding = new System.Windows.Data.Binding("ItemsSource") { Source = _listBox; }
// EDIT: Potential bugfix - you probably want to check the itemssource here just
// in case the behaviour is applied after the original ItemsSource binding has been evaluated - otherwise you might miss the change
OnItemsSourceWatcherPropertyChanged();
}
}
private void ScrollIntoView(object target)
{
// Set selected item and try and scroll it into view
_listBox.SelectedItem = target;
_listBox.ScrollIntoView(target);
}
}
You probably want to tidy it up a bit and also make sure that the event handler for CollectionChanged is removed when the ItemsSource changes.
Also you might want to call it SeekLastAddedItemBehaviour or SeekLastAddedItemBehavior - I tend to keep the US spelling since it matches Microsoft's spelling. I think SeekLastItem sounds like it will scroll to the last item in the list rather than the last added item
You could reference the view in the view model using GetView(). That also couples the view and view model.
var myView = GetView() as MyView;
myView.MyListBox.DoStuff
Another option is to create a behavior. This is an example of how to use a behavior to expand a TreeView from the view model. The same could be applied to a ListBox.
Actually, there is an easier way to achieve this, without any of the above.
Just extend your Listbox with the following:
namespace Extensions.Examples {
public class ScrollingListBox : ListBox
{
protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
int newItemCount = e.NewItems.Count;
if (newItemCount > 0)
this.ScrollIntoView(e.NewItems[newItemCount - 1]);
base.OnItemsChanged(e);
}
}
}
}
Then in Xaml, Declare the Location of your extension class as so:
xmlns:Extensions="clr-namespace:Extensions.Examples"
And when you create your listbox, instead of using
<Listbox></Listbox>
Just use your extended class
<Extensions:ScrollingListBox></Extensions:ScrollingListBox>

Bound Button Not Enabling After Background Worker Process Completes

I have a background worker process that starts provisioning a new client for our system. Here is what the DoWork method looks like:
ProvisioningManager manager = new ProvisioningManager(false)
{
};
System.Windows.Application.Current.Dispatcher.Invoke((Action)(() =>
{
this.MaxSteps = manager.MaxProgress;
}));
manager.StatusUpdated += new ProvisioningManager.StatusUpdatedHandler(manager_StatusUpdated);
manager.TaskCompleted += new ProvisioningManager.TaskCompleteHandler(manager_TaskCompleted);
manager.ProvisionClient();
while (!manager.Completed)
{
System.Threading.Thread.Sleep(100 * 60);
}
Basically it creates the manager that handles talking to the different sub-systems which provision the client.
Now I have a status update event and completed event for the provisioning manager. When the TaskCompleted event fires I want to be able to set a property on my display object so that the finish button in the wizard is enabled:
void manager_TaskCompleted(object sender, ProvisioningManager.Task taskType)
{
System.Windows.Application.Current.Dispatcher.Invoke((Action)(() =>
{
this.ProvisioningComplete = true;
}));
}
The XAML for the button looks like this:
<wizard:WizardPage Header="Provisioning Client..."
ShowBack="False"
AllowBack="False"
AllowFinish="{Binding Source={StaticResource ResourceKey=dataObject}, Path=ProvisioningComplete}"
Loaded="Provisioning_Loaded">
</wizard:WizardPage>
This isn't working. Even though I make sure to hit the dispatcher thread to set the property of the display object it doesn't actually change the button to enabled until I click on the window. Is this a bug in AvalonWizard or am I not on the correct thread to set an INotifyPropertyChanged? Is there a way to hack this; basically can I programmatically focus the window without the mouse click?
I tired placing that while loop in the DoWork method so that I could use the BackgroundWorker's completed method:
void provisioningWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
System.Windows.Application.Current.Dispatcher.Invoke((Action)(() =>
{
this.ProvisioningComplete = true;
}));
}
That doesn't work either. What gives?!
Update
Here is the requested static resource instantiation for the display object:
<Window.Resources>
<ObjectDataProvider x:Key="dataObject" ObjectType="{x:Type winDO:NewClientWizardDO}" />
</Window.Resources>
Update II
Here is the property and property change firer:
public bool ProvisioningComplete
{
get { return this._ProvisioningComplete; }
set
{
this._ProvisioningComplete = value;
this.NotifyPropertyChanged("ProvisioningComplete");
}
}
protected void NotifyPropertyChanged(params string[] propertyNames)
{
if (this.PropertyChanged != null)
{
foreach (string propertyName in propertyNames)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
sorry if I don't understand something, but is the ProvisioningComplete property marked as "volatile"? If not then this might be the problem.
So I couldn't find out exactly why I was having this issue. I tried setting focus to the window, the button, etc. I tried multiple ways of letting the view know the viewmodel had updated. Basically every suggestion I could find on the web didn't work. It almost seems like a bug.
A smarty on my team suggested faking a mouse click on the window. His idea was that since all it took to activate the button was a simple mouse click on the screen then faking one should have the same effect. I thought (and think) that this hack was ridiculous. I did try it out just to see if I could call it a "solution".
Well, it worked. We had this same problem in another one of our wizards (not AvalonWizard but a homegrown one). I think there has to be some underlying issue with the way the window redraws after a background thread updates objects that are bound to the UI.
Anyhow, the way I found to solve this issue is with the following hack-tastic code.
//import user32.dll and setup the use of the mouse_event method
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
/// <summary>
/// Watches for properties to change on the data object, mainly the ProvisioningComplete method
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void DataObject_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "ProvisioningComplete":
//if the provisioning is completed then we need to make the finish button selectable.
if (this.DataObject.ProvisioningComplete)
{
System.Windows.Application.Current.Dispatcher.Invoke((Action)(() =>
{
//give the window focus
this.Focus();
//update the layout
WizardPageProvisioningClient.UpdateLayout();
//fake mouse click 50 pixels into the window
mouse_event(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, (uint)(this.Left + 50), (uint)(this.Top + 50), 0, 0);
}));
}
break;
}
}
I've tested this when the window is not the active window and when the user leaves the window as selected. The focus method seems to take care of this issue when the window isn't active. Our QA team hasn't run a complete test against the UI so I can't say if there is any situations where it doesn't work, but it seems to be the best solution that I've come up with to date.
I'm open to any other suggestions if anyone out there has a better idea of what could be causing the button to not update.

Force WPF DataGrid to regenerate itself

I have a custom control which inherits from DataGrid and is basically a 2D DataGrid (accepts an ItemsSource with two dimensions, such as double[,] ).
I added a specific DependencyProperty which is ColumnHeaders and RowHeaders so I can define them.
Here is how it works right now:
I bind a 2D ItemsSource to the DataGrid
A wrapper method will take this source to transform it to a classic IEnumerable bindable to the actual datagrid's ItemsSource
Each row / column auto-generated is done using the events AutoGeneratingColumn & AutoGeneratingRow in order to define their header
The problem here:
When I initialize the DataGrid, everything works fine.
After that, one of the use-cases of my application defines that only the column headers can change (by modifying the DependencyProperty ColumnHeaders
And, whatever I do here, the DataGrid won't re-autogenerate its columns (and therefore, headers won't be changed in any way).
So, is there a way to ask the DataGrid something like "Hey, I want you to restart from scratch and regenerate your columns" ? Because for now, I can't reach the AutoGeneratingColumn event, and calling a method such as InvalidateVisual will just redraw the grid (and not regenerate columns).
Any ideas here?
I'm not sure that we need some code but... I'll put some so nobody asks for it :D
/// <summary>
/// IList of String containing column headers
/// </summary>
public static readonly DependencyProperty ColumnHeadersProperty =
DependencyProperty.Register("ColumnHeaders",
typeof(IEnumerable),
typeof(FormattedDataGrid2D),
new PropertyMetadata(HeadersChanged));
/// <summary>
/// Handler called when the binding on ItemsSource2D changed
/// </summary>
/// <param name="source"></param>
/// <param name="e"></param>
private static void ItemsSource2DPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
FormattedDataGrid2D #this = source as FormattedDataGrid2D;
#this.OnItemsSource2DChanged(e.OldValue as IEnumerable, e.NewValue as IEnumerable);
}
// (in the constructor)
AutoGeneratingColumn += new EventHandler<DataGridAutoGeneratingColumnEventArgs>(DataGrid2D_AutoGeneratingColumn);
void DataGrid2D_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
DataGridTextColumn column = e.Column as DataGridTextColumn;
column.Header = (ColumnHeaders == null) ? columnIndex++ : (ColumnHeaders as IList)[columnIndex++]; //Header will be the defined header OR the column number
column.Width = new DataGridLength(1.0, DataGridLengthUnitType.Auto);
Binding binding = column.Binding as Binding;
binding.Path = new PropertyPath(binding.Path.Path + ".Value"); // Workaround to get a good value to display, do not take care of that
}
Reset your ItemsSource and it should redraw your DataGrid
void ResetDataGrid()
{
var temp = myDataGrid.ItemsSource;
myDataGrid.ItemsSource = null;
myDataGrid.ItemsSource = temp;
}
You might also be able to refresh the binding, but I haven't tested it to see if this will actually regenerate the DataGrid:
void ResetDataGrid()
{
myDataGrid.GetBindingExpression(DataGrid.ItemsSourceProperty).UpdateTarget();
}
Toggling AutogeneratedColumns off and then on will cause the columns to be automatically generated again.
dataGrid.AutoGenerateColumns = false;
dataGrid.AutoGenerateColumns = true;

Resources