Saturday, 27 December 2014

WPF binding to a dynamic collection of dynamic objects



Simply(ish) put, what do I need to do to implement two-way binding between my UI and my custom objects, including the collections of objects within those objects (and the collections within the objects in those collections) so that I can display all the data in a DataGrid like format? I'm guessing I need to build a DataTemplate and probably need a ViewModel. I've never done either. For the DataTemplate, I'm not sure which UIElement in the tree to apply that to and I'm also not sure how to specify the binding (either in XAML or code). I've tried setting the DataContext for a StackPanel and then adding bindings to the controls I add to the StackPanel's Children. I think at one point, I'd gotten it displaying the data but couldn't get the UI to update the object; I also had to do it manually (just wrote C# code for TextBox objects, set their values and added them). I want to generate these dynamically so I can do it with any of my objects and their member collections' objects, and so on.


I've found dozens of questions similar to my issue but they lead me down a rabbit hole of hours' worth of reading tutorials and MSDN articles, with no solution. I suppose part of my problem is that I don't know exactly which of the many potential solutions are the best to ensure future extensibility (and code re-use). I'm using VS2013 with C#, targeting .NET4.5. Thanks in advance just for bothering to read this whole thing.


Clarification: I have a class called Waffle. Waffle has String Name, String Type, Dictionary<string, string> CustomAttributes and TrulyObservableCollection<Property> PropertyCollection. Those are populated by a custom XmlReader pointed to an XML file with a <waffle> as the root element and some attributes. Waffle.PropertyCollection is populated with <properties>'s sub-elements, which are read intoProperty` objects. (Link to TrulyObservableCollection).



<waffle Name="My Belgian Waffle" Type="Homemade" MyCustomAttr="custom data">
<properties>
<flour brand="Kirkland" source="CostCo">Plain old white flour</flour>
<height unit="inch">1</height>
<weight unit="lbs">.5</weight>
</properties>
</waffle>


Those Property-object sub-elements are not predefined; they're named arbitrarily. There are few pre-defined attributes, so most are put in Property.CustomAttributes. The element's name goes to Property.Name and its value goes to Property.Value, both are of type String. (I'll deal with converting them to other primitive types as the UI calls for it; for the data storage, String is fine.)


Everything below is just background information, in case this isn't quite making sense.




None of the classes are serializable yet, but that's also a goal; I'm not sure if doing that gives me better options for my binding. I know I can bind directly to XML with XmlDataProvider but the application will play host to hundreds of objects at a time, along with a corresponding image/icon on a custom control. It's also an MDI application so each document may have hundreds of Waffle objects; I'm not sure if keeping them in XML data is the best idea as the UI will need to be able to perform sorting and filtering on hundreds at a time. Each Waffle in each document's TrulyObservableCollection<Waffle> Waffles will be associated with a control in the document so its individual data is displayed in a docked "properties" window where it can be edited.


Originally, this was a Windows Forms application but the PropertyGrid needed static attributes and a custom type descriptor... it was getting too messy to be worth using a PropertyGrid outside its intended usage. I did some reading on WPF and decided that since I eventually wanted a WPF UI, I might as well just switch now.


My properties "window" is just an AvalonDock 2.0 LayoutAnchorable element. I just have no idea which WPF elements beneath it to bind to what and what to specify in my MainWindow.xaml vs MainWindow.xaml.cs vs the code-behind. The vast majority of the MSDN examples that appeared close to what I need are only shown in XAML (like the seemingly great idea to use ObjectDataProvider) and/or deal with a predictable set of object members/XML nodes.


Upon selecting a WaffleControl in the document, two (or three) different 'LayoutAnchorable' elements will be populated in a grid-like format: One for the Waffle object's static members and CustomAttributes and another for the Waffle.PropertyCollection Property objects. All the Property objects will be in a horizontally-scrollable column formation and/or in just in two columns, on each for Property.Name and 'Property.Value' with that third LayoutAnchorable being populated with all the other Property data upon being selected in the grid.


I also want those grid rows to have content-dependent editing controls. For example, it might display:



Property Name | Property Value
Waffle Name | My Belgian Waffle
Image | mywaffle.jpg


The "Property Name" values would just be plain-text so the property's name can be changed. The "Property Value" for "Image" would show an image selector a la System.Drawing.Design.ImageEditor. I'll build whatever custom controls I need to do this but I'm stuck on how to build the UI portion. Waffle and Property both implement INotifyPropertyChanged.




Here's the Waffle class:



public class Waffle : INotifyPropertyChanged
{
string m_Name, m_Type;

public String WaffleType
{
get
{
return this.m_Type;
}
set
{
if(value != this.m_Type)
{
this.m_Type = value;
RaisePropertyChangedEvent();
}

}
}

public String Name { get; set; } //same as above, shortened for brevity's sake

public TrulyObservableCollection<string, string> WaffleCustomAttributes { get; set; } //same as above, shortened for brevity's sake
public TrulyObservableCollection<Property> PropertyCollection { get; set; } //same as above, shortened for brevity's sake

private Lazy<TrulyObservableCollection<string, string>> lazyWaffleCustomAttributes;
private Lazy<TrulyObservableCollection<Property>> lazyPropertyCollection;

public event PropertyChangedEventHandler PropertyChanged;

public Waffle() { Initialize(); }
//other constructors do the same, just with parameters. removed for brevity

private void Initialize()
{
this.lazyWaffleCustomAttributes = new Lazy<TrulyObservableCollection<string, string>>();
this.WaffleCustomAttributes = this.lazyWaffleCustomAttributes.Value;
this.lazyPropertyCollection = new Lazy<TrulyObservableCollection<Property>>();
this.PropertyCollection = this.lazyPropertyCollection.Value;
}
private void RaisePropertyChangedEvent([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}

No comments:

Post a Comment