dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.02k stars 4.67k forks source link

[API Proposal]: Create an dynamic MockObject #59729

Open msedi opened 2 years ago

msedi commented 2 years ago

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 PropertyGridbecause 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 PropertyCollectionhas 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:

dotnet-issue-labeler[bot] commented 2 years ago

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

ghost commented 2 years ago

Tagging subscribers to this area: @safern See info in area-owners.md if you want to be subscribed.

Issue Details
### 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: ```cs class MockObject : INotifyPropertyChanged, ICustomTypeDescriptor { public ObservableCollectionObjects {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 ```C# namespace System.ComponentModel { public sealed class MockObject : DynamicObject, INotifyPropertyChanged, ICustomTypeDescriptor, { public ObservableCollection Items {get;} = new(); } public sealed class MockObject : DynamicObject, INotifyPropertyChanged, ICustomTypeDescriptor, { public ObservableCollection 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(A,D); Additionally, the type of merge (intersect and union) can be given as a parameter. ``` ### API Usage ```C# 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(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?
Author: msedi
Assignees: -
Labels: `api-suggestion`, `area-System.ComponentModel`, `untriaged`
Milestone: -
safern commented 2 years ago

@JeremyKuhne @RussKie @DustinCampbell for thoughts as this is tight to winforms designer.

msedi commented 2 years ago

Just to make sure, I took the behavior of the PropertyGrid as an example of what I'm thinking of: An object that mimics the access to a collection of objects of the same or of different types. I already have a running example that I can show and share here (but I'm not fully satisfied) which can be discussed.

RussKie commented 2 years ago

I'm not quite sure I understand the ask here, e.g. why does MockObject object need to be part of the .NET runtime (or Windows Forms for that matter) API, and how it would help. If there is some extensibility endpoint is missing in ICustomTypeDescriptor then we can certainly discuss this.

davidfowl commented 2 years ago

Couldn't you build your own dynamic object that accomplishes this? Why does it need to be in the runtime?

msedi commented 2 years ago

@davidfowl: Of course you are right, I could do it myself. I was just thinking if this is something that could be useful for others too. What do you think would be the right repository? Just in case: What would be the right repository, where to you keep System.ComponentModel for example or where would you put it?

msedi commented 2 years ago

@RussKie: Yes, the runtime is for sure the wrong place. I was just curious if this component would be something that could be useful for others too.

I think my only problem is (because I can build the rest myself), that there is no event on the ICustomTypeDescriptor that makes it "dynamic". So when you add or remove items from the MockObject that enforces the MockObjects to reevaluate its properties, attributes and events I cannot inform the outside to reevaluate. The same is true for the DynamicObject.

It can be seen again as an example in the PropertyGrid. If I attach a MockObject on the PropertyGrid as SelectedObject it gets is items evaluated and displayed. Once I add an item it does not get reevaluated automatically. I need to unassign (setting SelectedObject to null) and assign it again.

For the PropertyGrid this is not problem, because I can do it myself, but I don't know that something has changed.

I hope my explanation is somehow clear and understandable?