Skclusive.Blazor.Samples icon indicating copy to clipboard operation
Skclusive.Blazor.Samples copied to clipboard

How do I create state nodes that have child nodes with actions?

Open FieldMarshallVague opened this issue 3 years ago • 20 comments

Hi Skclusive,

Sorry if this isn't the right way to ask, but I've been struggling with something. I need to track an object that has child-objects that have their own behaviours. Trunck > Branch > Leaf. They all have their own EditName/EditBranches actions, so I can't use the DTO (snapshot) class as the type in the view layer (razor file)

So, Tree would have a list of Branches, I have Tree as a child of AppState, which is tracked and works properly (since it's on the root). But when I get to adding Branch, this is a child of Tree, not of AppState, so my interface is

ITree {
  List<IBranch> Branches
}

IBranchActions {
  EditName(string)
}

IBranchSnapshot {
  string Name
}

IBranch : IBranchSnapshot, IBranchActions

Now, IBranch needs to have an 'EditName' method, which is part of the Actions interface, this is what is implemented on the Proxy, not the snapshot.

Do you have any examples of this nested structure? I cannot see how to do it and have been circulating for a while now. I've looked at Todo and Flightfinder, but this does not seem to have anything like this. Is it even possible? Will it ever be? Or am I making a mistake?

I thought maybe I just put everything in the root, but you can imagine that would get very messy and tracking the IDs (or whatever) to link them back up would be a nightmare.

I'd really appreciate some advice on this. Thanks!

FieldMarshallVague avatar Nov 27 '20 19:11 FieldMarshallVague

hi. thanks for trying out state tree. yes. it is possible to define the above nested structure with actions specific to node types. the nesting can go any level.

i just added sample test for your use case here https://github.com/skclusive/Skclusive.Mobx.StateTree/tree/master/test/StateTree.Tests/Models/Nested

and the test here https://github.com/skclusive/Skclusive.Mobx.StateTree/blob/master/test/StateTree.Tests/TestNested.cs


            var root = RootType.Create(new RootSnapshot
            {
                Tree = new TreeSnapshot
                {
                    Branches = new IBranchSnapshot []
                    {
                        new BranchSnapshot { Name = "branch 1" },

                        new BranchSnapshot { Name = "branch 2" }
                    }
                }
            });

            Assert.NotNull(root);

            Assert.NotNull(root.Tree);

            Assert.Equal(2, root.Tree.Branches.Count);

            Assert.Equal("branch 1", root.Tree.Branches[0].Name);

            Assert.Equal("branch 2", root.Tree.Branches[1].Name);

            root.Tree.AddBranch(new BranchSnapshot { Name = "branch 4 (typo)" });

            Assert.Equal(3, root.Tree.Branches.Count);

            Assert.Equal("branch 4 (typo)", root.Tree.Branches[2].Name);

            root.Tree.Branches[2].EditName("branch 3");

            Assert.Equal("branch 3", root.Tree.Branches[2].Name);

hope this helps

skclusive avatar Nov 28 '20 18:11 skclusive

Awesome! Thanks so much for this. I'll test it tomorrow and let you know. Cheers, really helps out.

FieldMarshallVague avatar Nov 29 '20 19:11 FieldMarshallVague

Thanks again, that's working for the behaviours, but now I can't treat the objects as the snapshots/dto types. If I pass the Tree (for example) to a component, using root.Tree, the type is ITree (behaviours). How do you access the node as its snapshot type? Obviously there is some merging going on 'under the hood', but how do I cast, say root.Tree.Branches[0] as the BranchSnapShot type?

UPDATE: ahhh, If I use ITree.GetSnapshot<ITreeSnapshot>() will that return the snapshot with all the data? I'll try that.

Another question, though, the example given does not have nodes that have both data and behaviour. Only the Branch node has this.

If you were to add a Leaf node with data and behaviour (so both Branch and Leaf had both), would that work? That is 90% of my use-case.

UPDATE 2: I have this working now. I added the properties to the IBranch and ILeaf interfaces, which repeated the ones from the snapshot interface. The library 'merges' them and I can retrieve the snapshot using the GetSnapshot method (I only just found this today, but it has made the difference).

Thanks for your help. I'm pretty pleased with this so far.

FieldMarshallVague avatar Dec 01 '20 14:12 FieldMarshallVague

glad you got the answers yourself :-) someday the verbosity could be avoided using C# code generator.

i have been using it with pretty complex normalised data structure for my own project. recently tested with MobileBlazorBinding (netstandard2.0) as well. working well so far.

skclusive avatar Dec 01 '20 20:12 skclusive

That sounds like a good move forward. I was wondering how we could avoid so much boilerplate, but all redux-style implementations have them (AFAICT). These changes have stopped the Redux debugger working for me, though. It's throwing this error:

Deserialization of interface types is not supported. Type 'IRoot'

This happens when I try to step to a previous state, using the back arrow in the redux debugger. I can see why it's throwing it, but I am not sure what to do about it. That is, where to put the concrete type (Root), or shouldn't this be necessary?

I realised this was due to me not specifying interface->concrete types in the services, so I added

services.TryAddSingleTonEnumerable<JsonConverter, JsonTypeConverter<IBranch, Branch>>().

But then I got this:

The specified type System.Collections.Generic.List1[Demo.IBranch] must derive from the specific value's type Demo.IBranch[].`

So I added:

services.TryAddSingletonEnumerable<JsonConverter, JsonTypeConverter<IList<IBranch>, List<Branch>>>();

But I'm now at the point where I have no idea what the library really wants and where it wants it. I know in the BranchListType it specifies IBranch[], but I do not seem to be able to change that to a list without breaking things. My code is like so:

public readonly static IType<IBranch[], IObservableList<INode, IBranchState>> BranchListType = Types.Late("LateBranchListType", () => Types.List(BranchType));

Could you please give me a pointer?

FYI, before I created the complex object tree, the Redux tool was working well without this StateTreeTool, so I didn't think I needed it.

FieldMarshallVague avatar Dec 03 '20 13:12 FieldMarshallVague

Ah, I fixed that by setting the interface types to map to an array (I forgot it was talking to JS):

services.TryAddSingletonEnumerable<JsonConverter, JsonTypeConverter<IEnumerable<IBranch>, Branch[]>>();

And while this doesn't work when I have the shift-alt-D debugger attached (WASM debugger), I can run it without that and it doesn't throw an error until I try to step back in the redux history (similar to before, but different error).

Specified cast is not valid.

blazor.webassembly.js:1 Uncaught (in promise) Error: System.InvalidCastException: Specified cast is not valid. at Skclusive.Mobx.StateTree.ComplexType2[[Demo.IBranchSnapshot[], Demo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null],[Skclusive.Mobx.Observable.IObservableList2[[Skclusive.Mobx.StateTree.INode, Skclusive.Mobx.StateTree, Version=5.1.2.0, Culture=neutral, PublicKeyToken=null],[Demo.Models.IBranch, Demo.State, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]], Skclusive.Mobx.Observable, Version=5.1.2.0, Culture=neutral, PublicKeyToken=null]].ApplySnapshot(INode node, Object snapshot)

It doesn't like the casting of IBranch to BranchSnapshot at this point, I think.

FieldMarshallVague avatar Dec 03 '20 17:12 FieldMarshallVague

you are almost there. we have to only map the snapshot types. not the observable types. as below.

https://github.com/skclusive/Skclusive.Blazor.Samples/blob/master/Skclusive.Blazor.FlightFinder/FlightFinder.State/Extension/FlightFinderExtension.cs#L17

services.TryAddJsonTypeConverter<IRootSnapshot, RootSnapshot>();
services.TryAddJsonTypeConverter<IBranchSnapshot, BranchSnapshot>();
services.TryAddJsonTypeConverter<ITreeSnapshot, TreeSnapshot>();

can you try the above and let me know if that works?

skclusive avatar Dec 04 '20 00:12 skclusive

btw i guess you are using the latest version 5.1.2.

skclusive avatar Dec 04 '20 01:12 skclusive

Ah, yes, I already tried that, sorry, the code above was a desperate attempt to see if something else worked :) Sorry, that line was confusing, the error was more clear.

Yes, I'm using the latest versions 5.1.2.

Here is the error again, with some unnecessary bits removed:

Uncaught (in promise) Error: System.InvalidCastException: Specified cast is not valid. at Skclusive.Mobx.StateTree.ComplexType2[[DataTypes.IBranchSnapshot[]],[Skclusive.Mobx.Observable.IObservableList2[[Skclusive.Mobx.StateTree.INode, Skclusive.Mobx.StateTree],[Models.IBranch]], Skclusive.Mobx.Observable]].ApplySnapshot(INode node, Object snapshot) at Skclusive.Mobx.StateTree.ListType2[[DataTypes.IBranchSnapshot, DataTypes],[Models.IBranch, State]].ApplySnapshot(INode node, Object snapshot) `

It seems to be a problem with my BranchListType, which is defined like so:

public readonly static IType<IBranchSnapshot[], IObservableList<INode, IBranch>> BranchListType = Types.Late("LateBranchListType", () => Types.Optional(Types.List(BranchType), Array.Empty<IBranchSnapshot>()));

Looking at your examples, I can't see a problem, but maybe I have to do this a different way because it is a more complex structure?

FieldMarshallVague avatar Dec 04 '20 11:12 FieldMarshallVague

oh. this one. i used to get this error. then i fixed the definition. could not recollect now. will try the same structure and update you.

skclusive avatar Dec 04 '20 17:12 skclusive

Hi Skclusive, did your latest release of the samples have anything in relating to this?

FieldMarshallVague avatar Dec 07 '20 11:12 FieldMarshallVague

no. but i just created a new branch with related changes for this issue.

https://github.com/skclusive/Skclusive.Blazor.Samples/tree/Issue13

and this commit https://github.com/skclusive/Skclusive.Blazor.Samples/commit/e6dd2890e8781ff6bb05062f29f997e995ce2e64

i am not able to reproduce the issue. i could time travel in redux devtools with nested structure. may be i miss the use case, have a look and let me know your thoughts.

skclusive avatar Dec 07 '20 19:12 skclusive

Thanks! I'll take a look.

FYI, I can clone the repo now with no problems (not idea why it it didn't work the other times).

FieldMarshallVague avatar Dec 08 '20 10:12 FieldMarshallVague

OK, I think I see the problem. I've been using lists on my TreeSnapshot model, so have been declaring the branches as IEnumerable<IBranchSnapshot>. But this seems to be where the history navigation blows up. It can't cast from the IEnumerable<IBranchSnapshot> to the IBranchSnapshot[].

FYI, I've also tried just using 'Branchsnapshot' (not the interface), but this didn't help.

Where would I have to override/explicitly cast the type to be able to use IEnumerable? I'm using this model in lists elsewhere, so it's handy to define it as the base.

FieldMarshallVague avatar Dec 08 '20 11:12 FieldMarshallVague

I guess it is not straight forward. I will have to try with enumeration and update you.

skclusive avatar Dec 13 '20 11:12 skclusive

Ah, thankyou. That would be great.

FieldMarshallVague avatar Dec 13 '20 14:12 FieldMarshallVague

Just to be 100% clear, I am using IEnumerable<IBranchsnapshot> instead of IBranchSnapshot[]. I think you understood this, but didn't want to waste your time :)

(github hid the generic part before, because of the angle-bracket syntax)

FieldMarshallVague avatar Dec 15 '20 12:12 FieldMarshallVague

Happy new year :) I hope you had a good time.

I wondered if you had any luck in using an enumerable?

FieldMarshallVague avatar Jan 08 '21 16:01 FieldMarshallVague

Happy new year :-)

Missed the track.

could you please share a minimal repo to reproduce? Or a test case in MobxStateTree ?

skclusive avatar Jan 14 '21 17:01 skclusive

Hi Skclusive, yes, I'll try one or the other and let you know. Cheers.

FieldMarshallVague avatar Jan 15 '21 11:01 FieldMarshallVague