Best way to expose an object in winform for editing? - winforms

I have an object from a deserialized XML.
I want to visualize it and make it editable from the UI.
This seemed very easy in the beginning, but it's getting more confusing as I delve deeper into it.
My first approach was using a splitted panel:
- In the left, a TreeView.
- In the right, a ListView showing the value/object selected in the TreeView.
Te problem is that I don't know how to link everything (list and tree) with the object.
I populate the TreeView by going through each element in the object.
Edit: Is it possible to be implemented in CLR. I need to use some C++ code besides this.

So you have a TreeView object that shows TreeNodes, where every TreeNode show one System.Xml.XmlNode. The (sub-)Nodes of each TreeNode correspond with the Children of the XmlNode.
You'll have to decide what Text to display for the XmlNode, but that is a minor problem.
class XmlTreeNode : System.Windows.Forms.TreeNode
{
public XmlTreeNode(System.Xml.XmlNode xmlNode) : base()
{
this.XmlNode = xmlNode;
string textToDisplay = xmlNode.ToDisplayText();
this.Text = textToDisplay;
foreach (var childXmlNode in xmlNode.xmlNodeList.Cast<XmlNode>())
{
XmlTreeNode childNode = new XmlTreeNode(childXmlNode);
this.Nodes.Add(childNode);
}
}
public XmlNode XmlNode {get; private set;}
}
Of course an XmlNode does not have a method ToDisplayText(), so let's create an extension function for this. See extension methods demystified
static string ToDisplayText(this System.Xml.XmlNode xmlNode)
{
// TODO: what would you like to Display?
return xmlNode.Name;
}
Of course you want to be able to show these XmlTreeNodes in an XmlTreeNodeView:
class XmlTreeNodeView : System.Windows.Forms.TreeView
{
// default constructor: constructs empty XmlTreeNodeView:
public XmlTreeNodeView() : base() {}
// constructor fills the XmlTreeNodeView with the XmlNodes:
public XmlTreeNodeView(IEnumerable<XmlNode> xmlNodes) : base()
{
foreach (XmlNode xmlNode in xmlNodes)
{
this.Nodes.Add(new XmlTreeNode(xmlNode));
}
}
And of course you want to be notified if one of the nodes is clicked
public class XmlTreeNodeEventArgs : EventArgs
{
public XmlNode XmlNode {get; set;}
}
And in your XmlTreeView class:
public event EventHandler<XmlTreeNodeEventArgs> XmlNodeClicked;
protected virtual void OnXmlNodeClicked(XmlNode node)
{
return XmlNodeClicked?.Invoke(new XmlTreeNodeEventArgs() {XmlNode = node});
}
protected override void OnAfterSelect (System.Windows.Forms.TreeViewEventArgs e)
{
// get the XmlTreeNode that was clicked:
XmlTreeNode node = (XmlTreeNode)e.Node;
this.OnXmlNodeClicked(node);
}
The nice thing is, that you have to do pretty stupid things to get other things than XmlNodes in your XmlTreeView.
If you really want to prevent that something different than an XmlNode is added, your XmlTreeView should not inherit from TreeView, but from UserControl. The UserControl should show a TreeView inside it. Although this method works, you'll have to copy all TreeView functionality that you want to expose. I'm not sure if the extra work weighs up to the cost of extra security that your XmlTreeView contains only XmlTreeNodes.

Related

Prism 6.3: Reuse View with different View Model's

I'm in a scenario to reuse a view with two completly independent view models.
For example you can think a generic list view to show apples somewhere and somewhere else to show cars. Doesn't really matter.
In Prism.Forms for Xamarin im able to glue a view with a viewModel like this.
Container.RegisterTypeForNavigation<PageA, ViewModelA>("PageA1");
Container.RegisterTypeForNavigation<PageA, ViewModelB>("PageA2");
I can't find an equivalent in Prism WPF, can someone help me out?
The link that #AdamVincent posted and the "missing" methods are very useful for normal view/viewmodel navigation using the ViewModelLocationProvider. However when trying to use two view models for the same view they don't work. This is because inside the extension method there is a call that registers the viewmodel to the view for use by the ViewModelLocationProvider.
private static IUnityContainer RegisterTypeForNavigationWithViewModel<TViewModel>(this IUnityContainer container, Type viewType, string name)
{
if (string.IsNullOrWhiteSpace(name))
name = viewType.Name;
ViewModelLocationProvider.Register(viewType.ToString(), typeof(TViewModel));
return container.RegisterTypeForNavigation(viewType, name);
}
Internally, ViewModelLocationProvider.Register uses a dictionary to store the association between view models and views. This means, whe you register two view models to the same view, the second will overwrite the first.
Container.RegisterTypeForNavigation<PageA, ViewModelA>("PageA1");
Container.RegisterTypeForNavigation<PageA, ViewModelB>("PageA2");
So with the above methods, when using the ViewModelLocationProvider, it will always create an instance of ViewModelB because it was the last one to be registered.
Additionally, the next line calls RegisterTypeForNavigation which itself ultimately calls Container.RegisterType, is only passing the viewType.
To resolve this, I tackled it a different way using an Injection property. I have the following method to bind my viewmodel to my view
private void BindViewModelToView<TView,TViewModel>(string name)
{
if (!Container.IsRegistered<TViewModel>())
{
Container.RegisterType<TViewModel>();
}
Container.RegisterType<TView, TViewModel>(name,new InjectionProperty("DataContext", new ResolvedParameter<TViewModel>()));
}
We know each view will have a DataContext property, so the Injection property will inject the viewmodel directly into the DataContect for the view.
When registering the viewmodels, instead of using RegisterTypeForNavigation, you would use the following calls:
BindViewModelToView<PageA,ViewModelA>("ViewModelA");
BindViewModelToView<PageA,ViewModelB>("ViewModelB");
To create the view, I already have a method that I use to inject the appropriate view into my region, and it works using the viewname as the key to obtain the correct viewmodel instance.
private object LoadViewIntoRegion<TViewType>(IRegion region, string name)
{
object view = region.GetView(name);
if (view == null)
{
view = _container.Resolve<TViewType>(name);
if (view is null)
{
view = _container.Resolve<TViewType>();
}
region.Add(view, name);
}
return view;
}
Which I simply call with
var view = LoadViewintoRegion<PageA>(region,"ViewModelA");
and
var view = LoadViewintoRegion<PageA>(region,"ViewModelB");
So for normal single View/Viewmodels, I use the ViewModelLocationProvider.AutoWireViewModel property and where I have multiple viewmodels, I use this alternative approach.
2022/01/15 Update
First of all thanks a lot for Jason's answer, his answer is great, and I implemented it perfectly with reference to his design, but because of Prism version update, I made some changes
I have an single view with multiple viewmodel
Step 1
register your view region
<ContentControl prism:RegionManager.RegionName="{x:Static hard:RegionNames.PanelPosCameraRegion}"></ContentControl>
Step 2
coding your viewmodel
private IRegionManager _RegionManager;
private IUnityContainer _UnityContainer;
public ICommand LoadedCommand { get; set; }
public RoboticPageVM(IRegionManager regionManager, IUnityContainer unityContainer)
{
_RegionManager = regionManager;
_UnityContainer = unityContainer;
LoadedCommand = new DelegateCommand(LoadedCommandHandle);
}
private void LoadedCommandHandle()
{
BindViewModelToView<PanelPosMultiplexView, PanelPosCameraVM>("Camera");
BindViewModelToView<PanelPosMultiplexView, PanelPosAxisVM>("Axis");
LoadViewIntoRegion<PanelPosMultiplexView>(RegionNames.PanelPosCameraRegion, "Camera");
LoadViewIntoRegion<PanelPosMultiplexView>(RegionNames.PanelPosAxisRegion, "Axis");
}
private void BindViewModelToView<TView, TViewModel>(string registerName)
{
if (!_UnityContainer.IsRegistered<TViewModel>())
{
_UnityContainer.RegisterType<TViewModel>();
}
_UnityContainer.RegisterType<TView>(registerName, new InjectionProperty(nameof(UserControl.DataContext), new ResolvedParameter<TViewModel>()));
}
private void LoadViewIntoRegion<TView>(string regionName, string registerName)
{
IRegion region = _RegionManager.Regions[regionName];
object? view = region.GetView(registerName);
if (view == null)
{
view = _UnityContainer.Resolve<TView>(registerName);
}
if (!region.Views.Any(v => v.GetType() == typeof(TView)))
{
region.Add(view, registerName);
}
}

Fill a combobox in WPF with a list of web services

I have a control like this:
<ComboBox x:Name="ComboTipo"
Height="23"
SelectionChanged="ComboTipo_SelectionChanged"
Width="450"
Canvas.Left="609"
Canvas.Top="26" />
And my code is:
ComboTipo.DisplayMemberPath = "Descripcion";
ComboTipo.SelectedValuePath = "IdTipoPersona";
ComboTipo.ItemsSource = myWebServices.dameTipos();
My web services returns a list for this object, this class is created in automatic when i add the reference to the web services:
public partial class TipoPersona {
private short idTipoPersonaField;
private string descripcionField;
/// <comentarios/>
public short IdTipoPersona {
get {
return this.idTipoPersonaField;
}
set {
this.idTipoPersonaField = value;
}
}
/// <comentarios/>
public string Descripcion {
get {
return this.descripcionField;
}
set {
this.descripcionField = value;
}
}
}
But the problem is:
The combobox displays the data types for each element of the list, and i want display the Descripcion.
Can you help me plis! Thanks
What does IdTipoPersona look like? Is it a class you created? If so, you may need to reference the property that you want displayed. It would look something like this:
ComboTipoPersona.SelectedValuePath = "IdTipoPersona.Text";
Where Text would be replaced by the property. It is really hard to judge otherwise what is going on with knowing a little more about the object structure that myWebServices.dameTipos() returns.
EDIT
Ok I was able to simulate your problem and simulate a solution as well.
Your issue is in the Tipos class. There are a couple of things necessary when binding to a combobox with a custom class.
First off, you will want to add accessors and mutators (getters and setters) to IdTippoPersona and Descripcion.
You should add a constructor that assigns to those properties with parameters.
It is usually a good idea to add a default constructor.
The finished code will look like this:
public class Tipos
{
public int IdTipoPersona { get; set; }
public string Descripcion { get; set; }
public Tipos(int id, string descripcion)
{
IdTipoPersona = id;
Descripcion = descripcion;
}
}
I found the asnwer if someone needs it.
We need create a class intermediate class but we were working with entity framework, for this way, we need add the intermediate class like complex type in my model (entity framework).
And also we need override this class.
And it works so well.
Thanks for all #Goody

How do I get the Model instance from inside its wrapping ViewModel?

Short version:
If I have ViewModel, containing its Model object and exposing its properties, how do I get the model "back" after it has been edited? If the Model-inside-ViewModel is public, it violates encapsulation, and if it is private, I cannot get it (right?).
Longer version:
I am implementing a part of an application which displays collections of objects. Let's say the objects are of type Gizmo, which is declared in the Model layer, and simply holds properties and handle its own serialization/deserialization.
In the Model layer, I have a Repository<T> class, which I use to handle collections of MasterGizmo and DetailGizmo. One of the properties of this repository class is an IEnumerable<T> Items { get; } where T will be some of the Gizmo subtype.
Now since Gizmo doesn't implement INPC, I have created the following classes in ViewModel layer:
GizmoViewModel, which wraps every public property of a Gizmo so that setting any property raises PropertyChanged accordingly;
[**] RepositoryViewModel<T>, which has an ObservableCollection<GizmoViewModel> whose CollectionChanged is listened to by a method that handles Adds, Removes and Updates to the repository.
Notice that the Model layer has a "Repository of Models", while the ViewModel layer has a "ViewModel with an ObservableCollection of ViewModels".
The doubt is related to the [**] part above. My RepositoryViewModel.CollectionChangedHandler method is as follows:
void CollectionChangedHandler(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (var added in e.NewItems)
{
var gvm = added as GizmoViewModel;
if (gvm != null)
{
//// IS ANY OF THE ALTERNATIVES BELOW THE RIGHT ONE?
// Gizmo g = gvm.RetrieveModel(); ?? proper getter ??
// Gizmo g = GetModelFromViewModel(gvm); ?? external getter ??
// Gizmo g = gvm.Model; ?? public model property ??
_gizmo_repository.Add(g);
}
}
break;
....
Besides that, if anyone can detect any MVVM smell here, I'll be happy to know.
We can deal with our Models even outside the View and ViewModel layers, so leaving the model publicly accessible from ViewModel is I believe acceptable.
Let say you are creating the Models in "DataLayer" you can pass the instance of the Model to the ViewModel. To illustrate my point:
///Models ////////////////////////////
public interface IGizmo{}
public class Gizmo:IGizmo{}
public class SuperGizmo : IGizmo {}
public class SuperDuperGizmo : IGizmo { }
//////////////////////////////////////
public interface IGizmoViewModel<out T>
{
T GetModel();
}
public abstract class GizmoViewModelBase : IGizmoViewModel<IGizmo>
{
protected GizmoViewModelBase(IGizmo model)
{
_Model = model;
}
private readonly IGizmo _Model;
public IGizmo GetModel()
{
return _Model;
}
}
public class GizmoViewModel : GizmoViewModelBase
{
public GizmoViewModel(Gizmo model)
: base(model) { }
}
public class SuperDuperGizmoViewModel : GizmoViewModelBase
{
public SuperDuperGizmoViewModel(SuperDuperGizmo model)
: base(model){}
}
Your repository of Models will be updated on whatever updates it get from the ViewModel as long as you passed the same instance. So there is no need to have a repository of ViewModels to get the updates.
Reading your code, I think there is something of a mixup regarding your ViewModel and Model separation.
So, as I understand it, when your ObservableCollection of GizmoViewModel's changes, you are trying to add the Gizmo instance of the new item back to your Model?
I would approach this differently. You should create your Gizmo instances inside your Model layer, and when you do this you should add it to the Repository.
Otherwise, you haven't provided enough information - or rather, you have provided too much but it is the wrong sort of information. You need to describe the situation in which you want to do this, where these GizmoViewModels are created, etc.
From what I can see here, your GizmoViewModel has a dependency to your Repository<T>, so why not pass in the repository when you create your view model?
public class GizmoViewModel
{
private IRepository<Gizmo> _Repo;
//Underlying model (Doesn't implement INotifyPropertyChanged)
private Gizmo _Model;
//Wrapping properties
public int MyProperty
{
get { return _Model.Property; }
set
{
_Model.Property = value;
NotifyOfPropertyChange();
}
}
...
public GizmoViewModel(IRepository<Gizmo> repo)
{
_Repo = repo;
}
public void AddToRepo()
{
_Repo.Add(_Model);
}
...
It would be even better if these methods are inside the RepositoryViewModel base class. You can really go crazy with inheritance here. Perhaps something like this:
var gvm = added as IRepositoryViewModel;
if (gvm != null)
gvm.AddToRepo();
You can then simply call AddToRepo when you need to add the view model's underlying model to the repository.
Perhaps not the most elegant solution, however if encapsulation is what's worrying you, then you need to ensure that your dependencies are properly managed.
"If the Model-inside-ViewModel is public, it violates encapsulation"
Your assertion above is completely wrong and is killing your code.
By setting the Model property in ViewModel as private, you are forced to repeat your self ( code smells ), as you will need to define in your ViewModel, the same properties as you did for your Model, effectively transforming it into a Model class that mimics the Model it is supposed to expose to the View.
In MVVM the ViewModel role is to provide the View with all the presentation data and logic that it needs and for sure the Model is fundamental part of this data, by hidding it from the View you are killing MVVM.

How to use a factory for DataGrid.CanUserAddRows = true

I would like to use the DataGrid.CanUserAddRows = true feature. Unfortunately, it seems to work only with concrete classes which have a default constructor. My collection of business objects doesn't provide a default constructor.
I'm looking for a way to register a factory that knows how to create the objects for the DataGrid. I had a look at the DataGrid and the ListCollectionView but none of them seems to support my scenario.
The problem:
"I'm looking for a way to register a factory that knows how to create the objects for the DataGrid". (Because my collection of business objects doesn't provide a default constructor.)
The symptoms:
If we set DataGrid.CanUserAddRows = true and then bind a collection of items to the DataGrid where the item doesn't have a default constructor, then the DataGrid doesn't show a 'new item row'.
The causes:
When a collection of items is bound to any WPF ItemControl, WPF wraps the collection in either:
a BindingListCollectionView when the collection being bound is a BindingList<T>. BindingListCollectionView implements IEditableCollectionView but doesn't implement IEditableCollectionViewAddNewItem.
a ListCollectionView when the collection being bound is any other collection. ListCollectionView implements IEditableCollectionViewAddNewItem (and hence IEditableCollectionView).
For option 2) the DataGrid delegates creation of new items to the ListCollectionView. ListCollectionView internally tests for the existence of a default constructor and disables AddNew if one doesn't exist. Here's the relevant code from ListCollectionView using DotPeek.
public bool CanAddNewItem (method from IEditableCollectionView)
{
get
{
if (!this.IsEditingItem)
return !this.SourceList.IsFixedSize;
else
return false;
}
}
bool CanConstructItem
{
private get
{
if (!this._isItemConstructorValid)
this.EnsureItemConstructor();
return this._itemConstructor != (ConstructorInfo) null;
}
}
There doesn't seem to be an easy way to override this behaviour.
For option 1) the situation is a lot better. The DataGrid delegates creation of new items to the BindingListView, which in turn delegates to BindingList. BindingList<T> also checks for the existence of a default constructor, but fortunately BindingList<T> also allows the client to set the AllowNew property and attach an event handler for supplying a new item. See the solution later, but here's the relevant code in BindingList<T>
public bool AllowNew
{
get
{
if (this.userSetAllowNew || this.allowNew)
return this.allowNew;
else
return this.AddingNewHandled;
}
set
{
bool allowNew = this.AllowNew;
this.userSetAllowNew = true;
this.allowNew = value;
if (allowNew == value)
return;
this.FireListChanged(ListChangedType.Reset, -1);
}
}
Non-solutions:
Support by DataGrid (not available)
It would reasonable to expect the DataGrid to allow the client to attach a callback, through which the DataGrid would request a default new item, just like BindingList<T> above. This would give the client the first crack at creating a new item when one is required.
Unfortunately this isn't supported directly from the DataGrid, even in .NET 4.5.
.NET 4.5 does appear to have a new event 'AddingNewItem' that wasn't available previously, but this only lets you know a new item is being added.
Work arounds:
Business object created by a tool in the same assembly: use a partial class
This scenario seems very unlikely, but imagine that Entity Framework created its entity classes with no default constructor (not likely since they wouldn't be serializable), then we could simply create a partial class with a default constructor. Problem solved.
Business object is in another assembly, and isn't sealed: create a super-type of the business object.
Here we can inherit from the business object type and add a default constructor.
This initially seemed like a good idea, but on second thoughts this may require more work than is necessary because we need to copy data generated by the business layer into our super-type version of the business object.
We would need code like
class MyBusinessObject : BusinessObject
{
public MyBusinessObject(BusinessObject bo){ ... copy properties of bo }
public MyBusinessObject(){}
}
And then some LINQ to project between lists of these objects.
Business object is in another assembly, and is sealed (or not): encapsulate the business object.
This is much easier
class MyBusinessObject
{
public BusinessObject{ get; private set; }
public MyBusinessObject(BusinessObject bo){ BusinessObject = bo; }
public MyBusinessObject(){}
}
Now all we need to do is use some LINQ to project between lists of these objects, and then bind to MyBusinessObject.BusinessObject in the DataGrid. No messy wrapping of properties or copying of values required.
The solution: (hurray found one)
Use BindingList<T>
If we wrap our collection of business objects in a BindingList<BusinessObject> and then bind the DataGrid to this, with a few lines of code our problem is solved and the DataGrid will appropriately show a new item row.
public void BindData()
{
var list = new BindingList<BusinessObject>( GetBusinessObjects() );
list.AllowNew = true;
list.AddingNew += (sender, e) =>
{e.NewObject = new BusinessObject(... some default params ...);};
}
Other solutions
implement IEditableCollectionViewAddNewItem on top of an existing collection type. Probably a lot of work.
inherit from ListCollectionView and override functionality. I was partially successful trying this, probably can be done with more effort.
I've found another solution to this problem. In my case, my objects need to be initialized using a factory, and there isn't really any way to get around that.
I couldn't use BindingList<T> because my collection must support grouping, sorting, and filtering, which BindingList<T> does not support.
I solved the problem by using DataGrid's AddingNewItem event. This almost entirely undocumented event not only tells you a new item is being added, but also allows lets you choose which item is being added. AddingNewItem fires before anything else; the NewItem property of the EventArgs is simply null.
Even if you provide a handler for the event, DataGrid will refuse to allow the user to add rows if the class doesn't have a default constructor. However, bizarrely (but thankfully) if you do have one, and set the NewItem property of the AddingNewItemEventArgs, it will never be called.
If you choose to do this, you can make use of attributes such as [Obsolete("Error", true)] and [EditorBrowsable(EditorBrowsableState.Never)] in order to make sure no one ever invokes the constructor. You can also have the constructor body throw an exception
Decompiling the control lets us see what's happening in there.
private object AddNewItem()
{
this.UpdateNewItemPlaceholder(true);
object newItem1 = (object) null;
IEditableCollectionViewAddNewItem collectionViewAddNewItem = (IEditableCollectionViewAddNewItem) this.Items;
if (collectionViewAddNewItem.CanAddNewItem)
{
AddingNewItemEventArgs e = new AddingNewItemEventArgs();
this.OnAddingNewItem(e);
newItem1 = e.NewItem;
}
object newItem2 = newItem1 != null ? collectionViewAddNewItem.AddNewItem(newItem1) : this.EditableItems.AddNew();
if (newItem2 != null)
this.OnInitializingNewItem(new InitializingNewItemEventArgs(newItem2));
CommandManager.InvalidateRequerySuggested();
return newItem2;
}
As we can see, in version 4.5, the DataGrid does indeed make use of AddNewItem. The contents of CollectionListView.CanAddNewItem are simply:
public bool CanAddNewItem
{
get
{
if (!this.IsEditingItem)
return !this.SourceList.IsFixedSize;
else
return false;
}
}
So this doesn't explain why we we still need to have a constructor (even if it is a dummy) in order for the add row option to appear. I believe the answer lies in some code that determines the visibility of the NewItemPlaceholder row using CanAddNew rather than CanAddNewItem. This might be considered some sort of bug.
I had a look at IEditableCollectionViewAddNewItem and it seems to be adding this functionality.
From MSDN
The IEditableCollectionViewAddNewItem
interface enables application
developers to specify what type of
object to add to a collection. This
interface extends
IEditableCollectionView, so you can
add, edit, and remove items in a
collection.
IEditableCollectionViewAddNewItem adds
the AddNewItem method, which takes an
object that is added to the
collection. This method is useful when
the collection and objects that you
want to add have one or more of the
following characteristics:
The objects in the CollectionView are different types.
The objects do not have a default constructor.
The object already exists.
You want to add a null object to the collection.
Although at Bea Stollnitz blog, you can read the following
The limitation of not being able to add a new item when the source has no
default constructor is very well
understood by the team. WPF 4.0 Beta 2
has a new feature that brings us a
step closer to having a solution: the
introduction of
IEditableCollectionViewAddNewItem
containing the AddNewItem method. You
can read the MSDN documentation about
this feature. The sample in MSDN shows
how to use it when creating your own
custom UI to add a new item (using a
ListBox to display the data and a
dialog box to enter the new item).
From what I can tell, DataGrid doesn’t
yet use this method though (although
it’s a bit hard to be 100% sure
because Reflector doesn’t decompile
4.0 Beta 2 bits).
That answer is from 2009 so maybe it's usable for the DataGrid now
The simplest way I could suggest to provide wrapper for your class without default constructor, in which constructor for source class will be called.
For example you have this class without default constructor:
/// <summary>
/// Complicate class without default constructor.
/// </summary>
public class ComplicateClass
{
public ComplicateClass(string name, string surname)
{
Name = name;
Surname = surname;
}
public string Name { get; set; }
public string Surname { get; set; }
}
Write a wrapper for it:
/// <summary>
/// Wrapper for complicated class.
/// </summary>
public class ComplicateClassWraper
{
public ComplicateClassWraper()
{
_item = new ComplicateClass("def_name", "def_surname");
}
public ComplicateClassWraper(ComplicateClass item)
{
_item = item;
}
public ComplicateClass GetItem() { return _item; }
public string Name
{
get { return _item.Name; }
set { _item.Name = value; }
}
public string Surname
{
get { return _item.Surname; }
set { _item.Surname = value; }
}
ComplicateClass _item;
}
Codebehind.
In your ViewModel you need to create wrapper collection for your source collection, which will handle item adding/removing in datagrid.
public MainWindow()
{
// Prepare collection with complicated objects.
_sourceCollection = new List<ComplicateClass>();
_sourceCollection.Add(new ComplicateClass("a1", "b1"));
_sourceCollection.Add(new ComplicateClass("a2", "b2"));
// Do wrapper collection.
WrappedSourceCollection = new ObservableCollection<ComplicateClassWraper>();
foreach (var item in _sourceCollection)
WrappedSourceCollection.Add(new ComplicateClassWraper(item));
// Each time new item was added to grid need add it to source collection.
// Same on delete.
WrappedSourceCollection.CollectionChanged += new NotifyCollectionChangedEventHandler(Items_CollectionChanged);
InitializeComponent();
DataContext = this;
}
void Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
foreach (ComplicateClassWraper wrapper in e.NewItems)
_sourceCollection.Add(wrapper.GetItem());
else if (e.Action == NotifyCollectionChangedAction.Remove)
foreach (ComplicateClassWraper wrapper in e.OldItems)
_sourceCollection.Remove(wrapper.GetItem());
}
private List<ComplicateClass> _sourceCollection;
public ObservableCollection<ComplicateClassWraper> WrappedSourceCollection { get; set; }
}
And finally, XAML code:
<DataGrid CanUserAddRows="True" AutoGenerateColumns="False"
ItemsSource="{Binding Path=Items}">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Path=Name}"/>
<DataGridTextColumn Header="SecondName" Binding="{Binding Path=Surname}"/>
</DataGrid.Columns>
</DataGrid>
I just wanted to provide an alternate solution to using a BindingList. In my situtation, the Business objects was held in an IEntitySet in a portable project (Silverlight), which did not support IBindingList.
The solution, first and foremost, is to subclass the grid, and overwrite the coerce callback for CanUserAddRows to use IEditableCollectionViewAddNewItem:
public class DataGridEx : DataGrid
{
static DataGridEx()
{
CanUserAddRowsProperty.OverrideMetadata(typeof(DataGridEx), new FrameworkPropertyMetadata(true, null, CoerceCanUserAddRows));
}
private static object CoerceCanUserAddRows(DependencyObject sender, object newValue)
{
var dataGrid = (DataGrid)sender;
var canAddValue= (bool)newValue;
if (canAddValue)
{
if (dataGrid.IsReadOnly || !dataGrid.IsEnabled)
{
return false;
}
if (dataGrid.Items is IEditableCollectionViewAddNewItem v && v.CanAddNewItem == false)
{
// The view does not support inserting new items
return false;
}
}
return canAddValue;
}
}
And then use the AddingNewItem event to create the item:
dataGrid.AddingNewItem += (sender, args) => args.NewItem = new BusinessObject(args);
And if you care for the details, here is the reason why it is a problem in the first place. The coerce callback in the framework looks like this:
private static bool OnCoerceCanUserAddOrDeleteRows(DataGrid dataGrid, bool baseValue, bool canUserAddRowsProperty)
{
// Only when the base value is true do we need to validate that the user
// can actually add or delete rows.
if (baseValue)
{
if (dataGrid.IsReadOnly || !dataGrid.IsEnabled)
{
// Read-only/disabled DataGrids cannot be modified.
return false;
}
else
{
if ((canUserAddRowsProperty && !dataGrid.EditableItems.CanAddNew) ||
(!canUserAddRowsProperty && !dataGrid.EditableItems.CanRemove))
{
// The collection view does not allow the add or delete action
return false;
}
}
}
return baseValue;
}
You see how it gets the IEditableCollectionView.CanAddNew? That means that it only enables adding when the view can insert and construct an item. The funny thing is that when we want to add a new item, it checks the IEditableCollectionViewAddNewItem.CanAddNewItem instead, which only asks if the view supports inserting new items (not creating):
object newItem = null;
IEditableCollectionViewAddNewItem ani = (IEditableCollectionViewAddNewItem)Items;
if (ani.CanAddNewItem)
{
AddingNewItemEventArgs e = new AddingNewItemEventArgs();
OnAddingNewItem(e);
newItem = e.NewItem;
}

How to sort views in an ItemsControl in Prism / MEF?

I use prism v4 and MEF to load my modules. My modules contain a handful of views (MVVM) which are loaded in a ItemsControl/NavigationRegion automatically by MEF.
This works nicely, all items show up in the ItemControl. But I don't like the order in which they show. One module might contain several of the items, so changing the module load order is not enough by itself.
How can I sort the different views in the ItemsControl? Is there any way to sort them by some property?
I use prism V4, MEF and exploration due to attributes like in the StockTraderRI example.
This is actually baked into Prism4. Just apply the ViewSortHintAttribute to your views:
[ViewSortHint("100")]
class FirstView : UserControl { }
[ViewSortHint("200")]
class SecondView : UserControl { }
The default sort comparer on the regions will pick up this attribute and sort the views accordingly. You can put any string into the attribute but I tend to use medium sized numbers that allow me to easily put a new view in between existing ones.
Oh dang, this was way easier than I expected:
You can tell the region manager how to sort the views in a specific region. You just need to provide a compare function to the region.
This example sorts by a very stupid value, the function name:
private static int CompareViews(object x, object y)
{
return String.Compare(x.ToString(), y.ToString());
}
this._regionManager.Regions["MyRegion"].SortComparison = CompareViews;
Of course the region needs to be known to the region manager before you can set the SortComparison. So far the only workaround I found to achieve this was to defer to set the comparison function using the Dispatcher:
private readonly IRegionManager _regionManager;
[ImportingConstructor]
public ShellViewModel(IRegionManager regionManager)
{
this._regionManager = regionManager;
Dispatcher dp = Dispatcher.CurrentDispatcher;
dp.BeginInvoke(DispatcherPriority.ApplicationIdle, new ThreadStart(delegate
{
if (this._regionManager.Regions.ContainsRegionWithName("MyRegion"))
this._regionManager.Regions["MyRegion"].SortComparison = CompareViews;
}));
}
Of course one should use some more useful information than the class name for the sorting order, but this should be easy to solve (I'll just add an interface to all views which might be added to this region which provide a value to sort by).
I'm pretty sure you are looking for the CollectionViewSource. Bea provides some information on how to make use of it in the link.
From an MVVM stance this is how I use the ICollectionView within my ViewModel. The _scriptService.Scripts property is an ObservableCollection<T> getting wrapped in an ICollectionView which is returned to the View. The _view.Filter is being used to filter out items within the ICollection, thus changing the View. Similar to typing 'acc' and seeing all items that begin with 'acc' in your list.
public class ScriptRepositoryViewModel : AViewModel
{
private readonly IUnityContainer _container;
private readonly IScriptService _scriptService;
private readonly IEventAggregator _eventAggregator;
private ICollectionView _view;
public ScriptRepositoryViewModel(IUnityContainer container, IScriptService scriptService, IEventAggregator eventAggregator)
{
_container = container;
_scriptService = scriptService;
_eventAggregator = eventAggregator;
}
public ICollectionView Scripts
{
get
{
if (_view == null)
{
_view = CollectionViewSource.GetDefaultView(_scriptService.Scripts);
_view.Filter = Filter;
}
return _view;
}
}
}
Below is the code which takes care of the filtering, and is coming in via a DelegateCommand within Prism, this resides in the same ViewModel.
#region SearchCommand
public DelegateCommand<object> SearchCommand { get; private set; }
private String _search = String.Empty;
private void Search(object commandArg)
{
_search = commandArg as String;
_view.Refresh();
}
public bool Filter(object arg)
{
bool usingPrefix;
IScript script = arg as IScript;
if (script.FileType == ConvertPrefixToFileType(_search, out usingPrefix))
{
if (_search.Length == 2)
return true;
else
return CheckProperties(script, usingPrefix);
}
else
{
if (usingPrefix)
return false;
else
return CheckProperties(script, usingPrefix);
}
}
With the base functionality in place and making use of the ICollectionView you can apply your sorting as follows....
_view.SortDescriptions.Add(new SortDescription("PropertyName", direction));
More information on the sorting behavior can be found here, as there are some performance thoughts to keep in mind.
You could use either metadata or properties. It depends on whether you have control over the interface or not...
Views are displayed in the order they are added:
RegionManager.RegisterViewWithRegion("ListRegion", typeof(ListView));
RegionManager.RegisterViewWithRegion("ListRegion", typeof(ListView2));
RegionManager.RegisterViewWithRegion("ListRegion", typeof(ListView3));
will look like:
----region--|
| view3 |
| view2 |
| view |

Resources