dotnet / Comet

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

Rethinking State Management #158

Open JeroMiya opened 4 years ago

JeroMiya commented 4 years ago

Not sure if this is covered by the framework yet or not (if so it's not documented?) - state management seems a bit cumbersome in the current samples. Requiring the use of State or BindingObject subclasses seems like a lot of boilerplate for something that should be pretty simple. I'd like to see a couple of programming models supported:

Original sample:

    readonly State<int> clickCount = new State<int> (1);

    public MyPage() {
        Body = () => new VStack {
            new Text (() => $"Click Count: {clickCount}"),
            new Button("Update Text", () => {
                clickCount.Value++;
            }
        };
    }

Simplified POCO state management with SetState(Action setter) function:

    int clickCount = 1;
        int secondClickCount = 1;
    public MyPage() {
        Body = () => new VStack {
            new Text (() => $"Click Count: {clickCount}"),
            new Button("Update Text", () => SetState(() => clickCount++)),

                        // StatefulClick is a simple helper method that is the equivalent of the above
                        new Text (() => $"Click Count: {secondClickCount}"),
                        new Button("Update Text").StatefulClick(() => secondClickCount++),

                        new Button("Reset State").StatefulClick(() => {
                                // Set two state fields, only one view re-render
                                clickCount = 1;
                                secondClickCount = 1;
                       }),
        };
    }

External state management with NotifyStateChanged(), equivalent to SetState(() => {}):

public class MyPage : View {

        [Inject]
    public MyAppStateStore Store { get; set; } // perhaps a Redux pattern state management library, or something custom

    public MyPage() {
                Store.MyPageState.Subscribe(state => NotifyStateChanged());
        Body = () => new VStack {
            new Text (() => $"Click Count: {Store.MyPageState.ClickCount}"),
            new Button("Update Text", () => Store.DispatchClickAction()),
        };
    }
}

Going a bit further, if we change Body to take the state as an argument, we can make make this a bit more elegant:

public class MyPage : StatefulView<MyPageState> {

        [Inject]
    public MyAppStateStore Store { get; set; }

    public MyPage() {
                // i.e. StatefulView<TState>.ConnectState(Observable<TState> stateObservable)
                ConnectState(Store.MyPageState); 
        Body = (state => new VStack {
            new Text (() => $"Click Count: {state.ClickCount}"),
            new Button("Update Text", () => Store.DispatchClickAction()),
        });
    }
}

The nice thing about StatefulView<TState> would be that, given the framework is now explicitly aware of a view's state, it can serialize/deserialize it during hot reload to more easily implement a stateful hot reload.

Clancey commented 4 years ago

Originally, it was designed similar. I had StatefulView and the Body build passed in the State. You can see it in the old history. However it was removed since this is simpler long term. State is needed for basic data types, if you want to use them directly. And right now, I you can subclass BindingObject for complex objects or implement the interface. This is temporary! Eventually I want you to be able to do:

[BindingObject]
class MyObject
{
public string Foo{get;set;}
}

And that is it! Or maybe just let you subclass an interface?

Or I want some language/runtime changes that would eliminate both! But those would take longer.

Clancey commented 4 years ago

Eventually, I may be able to remove State as well using the same techniques! But that is long term. However I still shouldnt need any of the StatefulView<> or State stuff.

Also, [Inject] exists, its just called [Environment], it will grab anything already set. I will add some better examples of that later!

JeroMiya commented 4 years ago

How does Blazor do it? It seems almost magic. No state wrappers or attributes or interfaces. I'm guessing it just rerenders after any DOM event callback? That might be OK if you could opt-out of it and manage re-renders manually if desired (e.g. for performance).

Would it be possible to just reflect on the view classes themselves and sort of.. make the view or any of its public properties implicitly a [BindingObject] somehow?

JeroMiya commented 4 years ago

Also, would it be possible to support both models with a separate base class? For instance, if the explicit React/Flutter style SetState(...) method was a better model for an application's state management, could they opt-in to it?

gpapadopoulos commented 3 years ago
[BindingObject]
class MyObject
{
public string Foo {get;set;}
public int ClickCount {get;set;}
public void MyClickMethod() {....}
}

So [BindingObject] binds the whole of MyObject to my View? And how would one reference (and bind to) individual properties from individual controls within my view, such as Foo?

Perhaps via an argument passed to the Body as mentioned above? Such as below:

Body = (state => new VStack { new Text (() => $"Click Count: {state.ClickCount}"), new Button("Update Text", () => state.MyClickMethod()), });

So "state" would be an instance of MyObject? Just a thought