Skip to content

Proposal: Implement ViewModel proxy generator for POCO entity graph #1069

Description

@weitzhandler

Overview

I wonder if this has been considered before, we often have complex POCO entity graphs which we want to reuse in our XAML app.

It would be so much easier to have ViewModel wrappers generated for the entire entity graph, that includes:

  • Inheriting a common EntityViewModel<TEntity> base class
  • Wrapper properties for scalar values (similar to what's currently generated in ObservableProperty)
  • Wrapper ObservableCollection<T> properties for ICollection<T> etc. entity properties (notify parent for collection changes?)
  • Wrapper EntityViewModel<TEntity> properties for other entity navigations (notify parent?)
  • Wrapper ObservableCollection<EntityViewModel<TEntity> for entity-collection navigations (notify parent?)
  • Implement IChangeTracking / IRevertibleChangeTracking?

API breakdown

*All code is pseudo prototyping on the fly to give a general idea.

public abstract class ViewModelProxyBase(Action? parentPropertyChangedAction, Action? parentPropertyChangingAction = default)
    : ObservableValidator, INotifyParentPropertyChange;
{
    private Action? ParentPropertyChangedAction;
    private Action? ParentPropertyChangingAction;
}

public abstract class ViewModelProxyBase<TModel>(TModel model, Action? parentPropertyChangedAction, Action? parentPropertyChangingAction = default)
    : ViewModelProxyBase(parentPropertyChangedAction, parentPropertyChangingAction)
    where TModel : class
{
    public TModel Model => model;    
}

public abstract class ObservableProxyCollectionSimple<T> : ObservableCollection<T>
{
    public ObservableProxyCollectionSimple<T>(Action? parentPropertyChangedAction, Action? parentPropertyChangingAction = default)
    {        
        OnCollectionChanged += (sender, e) => parentPropertyChangedAction?.Invoke();
    }
}

public abstract class ObservableProxyCollectionComplex<T> : ObservableCollection<ViewModelProxyBase<T>>
{
    private Action? _parentPropertyChangedAction;
    private Action? _parentPropertyChangingAction;

    public ObservableProxyCollectionSimple<T>(Action? parentPropertyChangedAction, Action? parentPropertyChangingAction = default)
    {  
        _parentPropertyChangedAction = parentPropertyChangedAction;
        _parentPropertyChangingAction = parentPropertyChangingAction;
        OnCollectionChanged += OnCollectionChanged;
    }    

    private void OnCollectionChanged(...)
    {
        // removed items - stop tracking
        // new items - start tracking
    }    
}

Usage example

public class Entity
{
    public int Id { get; set; }
    public Reference? Reference { get; set; }
    public ICollection<Reference> { get; set; } = [];
}

public class Reference
{
    public string Name { get; set; }
    public bool IsEnabled { get; }
}

public partial class EntityViewModel(Entity entity, Action? parentPropertyChangedAction, Action? parentPropertyChangingAction = default)
    : ViewModelProxyBase<Entity>(entity, parentPropertyChangedAction, parentPropertyChangingAction)
{
}

public partial class ReferenceViewModel(Reference reference, Action? parentPropertyChangedAction, Action? parentPropertyChangingAction = default)
    : ViewModelProxyBase<Reference>(reference, parentPropertyChangedAction, parentPropertyChangingAction)
{
}

What's generated:

public partial class EntityViewModel(Entity entity, Action? parentPropertyChangedAction, Action? parentPropertyChangingAction = default)
    : ViewModelProxyBase<MyEntity>(entity, parentPropertyChangedAction, parentPropertyChangingAction)
{
    public int Id
    {
        get => Model.Id;
        set 
        {
            if(Equals(Model.Id, value))
                return;

            OnPropertyChanging();
            parentPropertyChangingAction?.Inovke();
            Model.Id = value;
            OnPropertyChanged();
            parentPropertyChangedAction?.Inovke();
        }
    }

    private ReferenceViewModel? _Reference;
    public ReferenceViewModel? Reference
    {
        get
        {
            if(_Reference == null && Model.Reference != null)
            {
                _Reference = new ReferenceViewModel(Model.Reference, this);
            }
            
            return _Reference;
        }
        set 
        {
            if(Equals(Model.Reference, value))
                return;

            OnPropertyChanging();
            parentPropertyChangingAction?.Inovke();

            if(_Reference != null)
            {
                _Reference.Parent = null;
            }
 
            value.Parent = this;
            Model.Reference = value.Model;
            OnPropertyChanged();
            parentPropertyChangedAction?.Inovke();
        }
    }

    public ObservableCollectionProxyComplex<Reference> References
    {
        ...
    }
}

public partial class ReferenceViewModel(Reference reference, INotifyParent? parent = default)
    : ViewModelProxyBase<Reference>(reference, parent)
{
    public string Name
    {
        get => Model.Name;
        set 
        {
            if(Equals(Model.Name, value))
                return;

            OnPropertyChanging();
            Model.Id = value;
            OnPropertyChanged();
        }
    }

    public bool IsEnabled => Model.IsEnabled;
}

Breaking change?

No

Alternatives

Unknown

Additional context

Help us help you

Yes, but only if others can assist

Activity

  1. changed the title [-]Implement ViewModel proxy generator for POCO entities[/-] [+]Implement ViewModel proxy generator for POCO entity graph[/+] on Mar 26, 2025
  2. changed the title [-]Implement ViewModel proxy generator for POCO entity graph[/-] [+]Proposal: Implement ViewModel proxy generator for POCO entity graph[/+] on Mar 26, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature request 📬A request for new changes to improve functionality

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions