Description
Background and motivation
When looking at the PropertyGrid
(WinForms), you have the ability to set multiple objects of the same or of different types as SelectedObjects
. What the grid does internally is a union or an intersection of the properties and display the list of "surviving" properties in a list. It is of course depending on the implementation (telerik for example has a bit more features on it).
Additionally the values of the properties are also shown. Thereby each property value is taken from each of the SelectedObjects' property value. If all values for each property value match, the value is displayed, if it doesn't match it is left empty.
While this is only a UI thing, many times I have the need to mimic this in code. For example, in my situation I have many windows displaying different content, with tools. There can be many tools on each display (e.g. drawing a circle). Then there is another window that groups all tools together (currently each tool group is a MockObject). This must be a dynamic behavior because tools can be added or removed, some displays support a tool, some not. What is necessary that if you want to activate a tool or if you want to modify properties of the tool (e.g. the circle color) you should need to walk through every display and set the properties for each and every tool (sometimes also this is neceessary). In almost 99% you want to select the properties for all displays, and this is where the MockObject come in.
Currently, if I have an object that I want to mimic I have to create a for loop for each property that distributes the values to all SelectedObjects
. On the other side, I have to run through all of this when a value of a single object has been changed.
My currently implementation look like this:
class MockObject : INotifyPropertyChanged, ICustomTypeDescriptor
{
public ObservableCollection<object>Objects {get;} = new();
}
I have hidden the inner logic because its to long to be displayed here.
This only work currently for the PropertyGrid
because this one does the reflection by using the ICustomTypeDescriptor
.
There is one thing that doesn't work though - there is no way the PropertyGrid detects changes in the Objects, because there is obviously no way a ICustomTypeDescriptor
can inform a listener that its PropertyCollection
has changed (?) - as least I haven't found a way, so that anytime the PropertyCollection changes, one need to unassign and reassign the same object(s) to the SelectedObjects
. I assume this is a missing link to work better (dynamically) with the PropertyGrid
.
One of the bigger problems with my solution is that it is not working in code, because the properties are only accessible and visible in the code editor when either using dynamic or when using reflection, writing directly into the PropertyCollection
which is error prone since you only get runtime exceptions.
API Proposal
namespace System.ComponentModel
{
public sealed class MockObject : DynamicObject, INotifyPropertyChanged, ICustomTypeDescriptor,
{
public ObservableCollection<object> Items {get;} = new();
}
public sealed class MockObject<T> : DynamicObject, INotifyPropertyChanged, ICustomTypeDescriptor,
{
public ObservableCollection<T> Items {get;} = new();
}
}
// Typeless implementation creates a MockObject from a set of objects (which can be of course also other mockobjects)
DateTime A, B, C;
BigInteger D;
var mock1 = MockObject.Create(A,B,C);
var mock2 = MockObject.Create(A,D);
// Typed Versions where the result is explicitly given
TInterface mock3 = MockObject.Create<TInterface>(A,D);
Additionally, the type of merge (intersect and union) can be given as a parameter.
API Usage
interface IPoint
{
int X {get; set;}
int Y {get; set;}
}
class Point : IPoint
{
public int X {get; set;}
public int Y {get; set;}
}
Point A = new(1,1), B new(2,2), C=new(3,3);
TInterface mock = MockObject.Create<T, TInterface>(A,B,C);
mock.X = 10;
mock.Y = 100;
// Would print 10 | 10 | 10
Console.WriteLine($"{A.X} | {B.X} | {C.X}");
// Would print 100 | 100 | 100
Console.WriteLine($"{A.Y} | {B.Y} | {C.Y}");
Risks
There are of course a few open questions:
- How shall the MockObject be generated (SourceGenerator, Expression Trees, Roslyn Runtime Code generation)?
- Can the dynamic behavior of the MockObject work with the PropertyGrid?
- Is it necessary to do it with a factory method or can objects be added dynamically.
- Performance ?
- Which entity would be responsible for this, I assume the runtime has nothing to do here?