Closed Martinn2 closed 1 year ago
Hi, given that you can choose the best approach that fits better for your project here are 3 different ways to achieve it in MauiReactor/C# that I can think of: 1) pass global state/parameters down to your component tree. For example, say you have a class that resembles your settings pass it down to your components:
class Settings
{
public int MySettingValue{get;set;}
}
class MyComponent
{
Settings? _settings;
public MyComponent Settings(Settings settings)
{
_settings = settings;
return this;
}
Render()
{
//use _settings and pass down to components used here _settings object
}
}
2) Use dependency injection to "inject" your global state/parameters:
interface IGlobalSettings //->implemented somewhere and registered in MauiProgram.cs
{
int MySettingValue {get;set;}
}
class MyComponent
{
Render()
{
var settings = Services.GetRequiredService<IGlobalSettings>();
//use settings, but no need to pass down to components used here (they can easily access the settings class with DI as well)
}
}
3) Use the "Parameters" feature provided by MauiReactor: it allows you to create a Parameter in a parent component and access it in read-write mode from child components:
public class CustomParameter
{
public int Numeric { get; set; }
}
internal class ParametersPage : Component
{
private readonly IParameter<CustomParameter> _customParameter;
public ParametersPage()
{
_customParameter = CreateParameter<CustomParameter>();
}
public override VisualNode Render()
{
return new ContentPage("Parameters Sample")
{
new VStack(spacing: 10)
{
new Button("Increment from parent", () => _customParameter.Set(_=>_.Numeric += 1 )),
new Label(_customParameter.Value.Numeric),
new ParameterChildComponent()
}
.VCenter()
.HCenter()
};
}
}
partial class ParameterChildComponent : Component
{
public override VisualNode Render()
{
var customParameter = GetParameter<CustomParameter>();
return new VStack(spacing: 10)
{
new Button("Increment from child", ()=> customParameter.Set(_=>_.Numeric++)),
new Label(customParameter.Value.Numeric),
};
}
}
for more info on this specific feature please take a look at:
Added to the documentation: https://adospace.gitbook.io/mauireactor/q-and-a/how-to-deal-with-state-shared-across-components
Thanks for reply. I have tried approaches 1 and 2 and in my case it is not 100% working. To demo this I created an app that has one page containing Header (using custom component) and content. I also have a shared state. Both page and Header can access and modify the shared state. Every change in state should be applied in both of them. What I achieved is that if state is modified from the page, child is rendered as well (nice). But when I do it in child, page has no way to find out because just Header is rendered again.
Here is a code for the app:
public class AppState
{
public string UserName { get; set; } = string.Empty;
}
class Header : Component
{
public override VisualNode Render()
{
var appState = Services.GetRequiredService<AppState>();
return new HorizontalStackLayout {
new Label(appState.UserName),
new MauiReactor.Button("Set from header").OnClicked(() =>
{
appState.UserName +="x";
Invalidate();
}),
};
}
}
class MainPage : Component
{
public override VisualNode Render()
{
var appState = Services.GetRequiredService<AppState>();
return new ContentPage
{
new VerticalStackLayout {
new Header(),
new Label(appState.UserName),
new MauiReactor.Button("Set from content").OnClicked(() =>
{
appState.UserName += "y";
Invalidate();
}),
}
};
}
}
The solution I think of is using IObervable (from Reactive UI, which I prefer over events) for every properties in shared state. On every componet that needs to use shared state I would subscribe to the properties that the component uses and rerender. Is it good solution ?
OK, I see, this is how I would do it using a pure MVU approach:
public class AppState
{
public string UserName { get; set; } = string.Empty;
}
class MainPage : Component<AppState>
{
public override VisualNode Render()
{
return new ContentPage
{
new VerticalStackLayout {
new Header()
.UserName(State.UserName)
.OnUserNameChanged(newUserName => SetState(s => s.UserName = newUserName)),
new Label(State.UserName),
new Button("Set from content").OnClicked(() => SetState(s => s.UserName += "y")),
}
};
}
}
class Header: Component
{
string _userName;
Action<string> _action;
public Header UserName(string username)
{
_username = username;
return this;
}
public Header OnUserNameChanged(Action<string> action)
{
_action = action;
return this;
}
public override VisualNode Render()
{
return new HorizontalStackLayout {
new Label(_username),
new Button("Set from header").OnClicked(_action(_username + "x"),
};
}
}
The above is a "pure" MVU approach and I guess that if you come from other backgrounds (MVVM or ReactiveUI) appears a bit weird, but is something you have to get familiar with sooner or later while planning to move to MVU libraries like MauiReactor.
The same can be realized using the other approaches as well. For example, your code is pretty much similar to the sample I wrote for the documentation: https://adospace.gitbook.io/mauireactor/components/component-parameters
The DI approach, you used, is well-suited to handle for example global settings that you edit on a page and read in others. In case you have to be notified of any change and respond accordingly you have to create a callback and subscribe in the OnMount() or OnPropsChanged override (let me know if you have more questions on this)
I understand your point. But in my real world scenario, it is more complicated because the element accessing state might be deeper in the visual tree (for example MainPage->Header->SearchComponent-> SearchCustomerComponent). In this case SearchCustomerComponent is the one using (reading and changing) the state so if I understand correctly I would have to propagate the OnUserNameChanged event up the visual tree to the MainPage, This seems too complex to me.
Furthermore my state might change without user interaction (lets say I use signalR to receive messages when UserName changes, which must update UI as well).
So in my real world I will probably do the following:
Is it correct approach to use in my case ?
Yes, your approach is correct, override OnMount() AND OnPropsChanged() to subscribe to callbacks and it will work (better to use callbacks instead of c# events).
I've developed many production applications using the above methods including passing the state as shown above to child components event down to a deep tree of components and in my case worked pretty well.
Regarding the Parameters featured offered by MauiReactor, I just want to let you notice that it could work also fine in your case as the parameters can be read-write by any component down the tree. For example, say you have this tree: MainPage->Header->SearchComponent-> SearchCustomerComponent you can create a Parameter class at MainPage level, and read-write it in the MainPage itself but also in the Header, SearchComponent, and SearchCustomerComponent. Say you receive a new value from the websocket, just get the parameter, update its value, and the component tree from the MainPage down to SearchCustomerComponent is invalidated automatically and re-rendered using the new value.
I'm just describing more approaches here so that other people interested in this topic could find the right one for their case too :)
In your samples every component has its own state object. I my case I have more like "application state" that determines state of the components. To be specific I am trying to control my IOT device and on settings page I want to be able to set visibility of item based on version of connected device (the version is part of application state). This version is also used on other pages.
What is the correct way to handle situations like this ?
Thanks in advance