dotnet / Comet

Comet is an MVU UIToolkit written in C#
MIT License
1.65k stars 117 forks source link

GenerateStateClass - deep wrappers and change notification #200

Closed DevronB closed 3 years ago

DevronB commented 3 years ago

Added generation of child state classes for annotated state class properties that return classes themselves. So nested classes will be wrapped with state and change notified all the way down.

I'd like to do some more work on this and wrap methods too, to properly mimic classes + State. Personally I'm leaning towards this solution over the potential Fody one as I think it suits my app model better. I'm looking at writing a watch app for Wear OS and I want to be able to suspend/resume bindings updated from a service, on certain events (like screen off/on) to minimize processing. We could also add methods like:

One issue I see with this is dealing with Collections and having state on those objects. I suspect this is where Fody would suit better as it will deal with all scenarios?

void StartAutoNotify(int interval=1000){...};
void StopAutoNotify(){...};

That use an internal timer to process dirty props.

Let me know if you think it's useful and to keep pushing PR's. I can always take it offline into my own source gen if you don't see something like this as part of the Comet features.

Working Sample:

public class POCOPlainChildChild
{
    public int ChildChildRides { get; set; }
    public string ChildChildCometTrain => "☄️".Repeat(ChildChildRides);
}

public class POCOPlainChild
{
    public int ChildRides { get; set; }
    public string ChildCometTrain => "☄️".Repeat(ChildRides);
    public POCOPlainChildChild PlainChildChild => new();
}

[GenerateStateClass]
public class POCOPlain
{
    public int Rides { get; set; }
    public string CometTrain => "☄️".Repeat(Rides);
    public POCOPlainChild PlainChild => new();
}

public class TMainPage : View
{
    static readonly POCOPlain POCOPlain = new();
    readonly POCOPlainState comet = new(POCOPlain);

    [Body]
    View body()
        => new VStack {
            new Text(()=> $"({comet.PlainChild.PlainChildChild.ChildChildRides}) rides taken:{comet.PlainChild.PlainChildChild.ChildChildCometTrain}")
                .Frame(width:300)
                .LineBreakMode(LineBreakMode.CharacterWrap)
                .FillHorizontal(),

            new Button("Ride the Comet Sync - TEST ☄️", ()=>{
                //comet.Rides++;
                comet.PlainChild.PlainChildChild.ChildChildRides++;
                //comet.OriginalModel.Rides++;
                comet.NotifyChanged();
                })
                .Frame(height:44)
                .Margin(8)
                .Background(Colors.Green)
                .Color(Colors.White)
                .TextAlignment(TextAlignment.Center)
                .FillHorizontal()
        };
}

And generated state class:

public partial class POCOPlainState :INotifyPropertyRead, IAutoImplemented 
{
    public event PropertyChangedEventHandler PropertyRead;
    public event PropertyChangedEventHandler PropertyChanged;

    public readonly NS.POCOPlain OriginalModel;

    bool shouldNotifyChanged = true;

    public POCOPlainState (NS.POCOPlain model)
    {
        OriginalModel = model;
        InitStateProperties();
        if (model is INotifyPropertyChanged inpc)
        {
            inpc.PropertyChanged += Inpc_PropertyChanged;
            shouldNotifyChanged = false;
        }
    }

    void Inpc_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        StateManager.OnPropertyChanged(sender, e.PropertyName, null);
        PropertyChanged?.Invoke(sender, e);
    }

    void NotifyPropertyChanged(object value, [CallerMemberName] string memberName = null){
        if (shouldNotifyChanged) {
            StateManager.OnPropertyChanged(this, memberName, value);
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(memberName));
        }
    }

    void NotifyPropertyRead([CallerMemberName] string memberName = null){ 
        InitDirtyProperty(memberName);
        StateManager.OnPropertyRead(this, memberName);
        PropertyRead?.Invoke(this, new PropertyChangedEventArgs(memberName));
    }

    /// <summary>
    /// Notifies Comet of all changes in the underlying model (OriginalModel) observed properties 
    /// </summary>
    public void NotifyChanged(){
            if (shouldNotifyChanged && ridesIsObserved) UpdateDirtyProperty("Rides");
            if (shouldNotifyChanged && cometTrainIsObserved) UpdateDirtyProperty("CometTrain");
            UpdateDirtyProperty("PlainChild");
    }

    void InitStateProperties(){
            PlainChild = new NS.POCOPlainChildState(OriginalModel.PlainChild);
    }

    void InitDirtyProperty([CallerMemberName] string memberName = null){
        switch (memberName){
            case "Rides":
                if (!ridesIsObserved){
                    ridesLastValue = OriginalModel.Rides;
                    ridesIsObserved = true;
                }
            break;
            case "CometTrain":
                if (!cometTrainIsObserved){
                    cometTrainLastValue = OriginalModel.CometTrain;
                    cometTrainIsObserved = true;
                }
            break;
        }
    }

    void UpdateDirtyProperty([CallerMemberName] string memberName = null){
        switch (memberName){
            case "Rides":
                if (ridesIsDirty){
                    ridesLastValue = OriginalModel.Rides;
                    NotifyPropertyChanged(ridesLastValue, memberName);
                }
            break;
            case "CometTrain":
                if (cometTrainIsDirty){
                    cometTrainLastValue = OriginalModel.CometTrain;
                    NotifyPropertyChanged(cometTrainLastValue, memberName);
                }
            break;
            case "PlainChild":
                PlainChild.NotifyChanged();
            break;
        }
    }

    bool ridesIsObserved = false;
    bool ridesIsDirty => ridesIsObserved && !OriginalModel.Rides.Equals(ridesLastValue);
    System.Int32 ridesLastValue;
    public System.Int32 Rides {
        get {
            NotifyPropertyRead();
            return OriginalModel.Rides;
        }
        set {
            OriginalModel.Rides = value;
            ridesLastValue = value;
            NotifyPropertyChanged(value);
        }
    }

    bool cometTrainIsObserved = false;
    bool cometTrainIsDirty => cometTrainIsObserved && !OriginalModel.CometTrain.Equals(cometTrainLastValue);
    System.String cometTrainLastValue;
    public System.String CometTrain {
        get {
            return OriginalModel.CometTrain;
        }
    }

    public NS.POCOPlainChildState PlainChild {
        get;
        private set;
    }
}