What code is needed to enable a Blazor child component to notify its parent component of state changes? I tried the following, which didn't work:
Helper class
public class NotificationHandler
{
public event Action? Dispatcher;
public void Notify() => Dispatcher?.Invoke();
public void Attach(Action dispatcher) => Dispatcher += dispatcher;
public void Release(Action dispatcher) => Dispatcher -= dispatcher;
}
Parent Component
#code
{
ChildComponent childComponent;
StateNotifier stateNotifier;
protected override Task OnInitializedAsync()
{
stateNotifier.Attach(StateHasChanged);
return base.OnInitializedAsync();
}
// Overloading StateHasChanged() - does it work this way?
protected new void StateHasChanged() // never gets called
{
DoSomething();
base.StateHasChanged();
}
}
<div>
<ChildComponent StateNotifier="stateNotifier" />
</div>
Child Component
#code
{
[Parameter]
public StateNotifier stateNotifier { get; set; }
async void OnSomeUserAction()
{
stateNotifier.Notify();
}
}
What code is needed to enable a Blazor child component to notify its
parent component of state changes?
Simplest way is to use an EventCallback.
Child Component
<button #onclick=NotifyParent>Notify</button>
#code {
[Parameter] public EventCallback OnSomethingHappened { get; set; }
async Task NotifyParent()
{
await OnSomethingHappened.InvokeAsync();
}
}
Parent Component
<ChildComponent OnSomethingHappened=#HandleSomethingHapppened />
#code {
async Task HandleSomethingHappened()
{
await DoSomethingElse();
// StateHasChanged() not needed when handling an event
// The Blazor engine will automatically
// run an Html diff after handling a component
// event.
// StateHasChanged();
}
}
Related
I'm working on a Xamarin Forms app and am using the MVVM Design.
the issue is when am navigating to another page using
Shell.Current.GoToAsync()
I disable the button to prevent Creating Multiple Pages or DB Operations.
but if I want to go back, I re-enable the buttons in the VM constructor, but the constructor never gets called which means the buttons are still disabled.
I tried to append the // in the Page route to remove the stack thinking that when I go back it will create a new instance Page and VM, but that did not work.
so can anyone help me resolving this problem.
thanks in advance.
Update:
VM Code
public RegisterViewModel()
{
Debug.WriteLine("Class Constructor", Class_Name);
//in case if disabled
RegisterButtonEnabled = true;
RegisterCommand = new Command(RegisterButtonOnClick);
}
public ICommand RegisterCommand { get; }
private bool registerButtonEnabled = true;
public bool RegisterButtonEnabled
{
get => registerButtonEnabled;
set
{
registerButtonEnabled = value;
OnPropertyChanged();
}
}
private async void RegisterButtonOnClick()
{
RegisterButtonEnabled = false;
//More Code
//and then go to Register Page
await Shell.Current.GoToAsync(nameof(RegisterPage));
}
and my xaml
<Button
Command="{Binding RegisterCommand}"
Text="{xct:Translate Register}"
Style="{StaticResource ButtonStyle}"
IsEnabled="{Binding RegisterButtonEnabled,Mode=OneWay}"/>
I had create a default shell project. And find something about the viewmodel. You can add the onappear and the ondisappear method to the viewmodel. Such as:
ViewModel:
public void OnAppearing()
{
RegisterButtonEnabled = true;
}
public void OnDisAppearing()
{
RegisterButtonEnabled = false;
}
Page.cs
ItemsViewModel _viewModel;
public ItemsPage()
{
InitializeComponent();
BindingContext = _viewModel = new ItemsViewModel();
}
protected override void OnAppearing()
{
base.OnAppearing();
_viewModel.OnAppearing();
}
protected override void OnDisappearing()
{
base.OnDisappearing();
_viewModel.OnDisAppearing();
}
I have two independent components in a Blazor wasm app between whichi am trying to communicate. under certain cases the communication fails and I cannot understand why.
The(simplified) setup is as follows
<ParentComponent>
<HeaderComponent>
<ProgressBar IsLoading="<Set by IsLoading property from header>" />
</HeaderComponent>
<ResultContainer />
</ParentComponent>
The code behind looks something like this:
public class ResultContainerStateManager
{
public event Action OnLoadStart;
public event Action OnLoadFinish;
public NotifyLoadStart() => this.OnLoadStart?.Invoke();
public NotifyLoadFinish() => this.OnLoadFinish?.Invoke();
}
public partial class HeaderComponent
{
[Inject]
public ResultContainerStateManager ResultContainerStateManager { get; set; }
private bool IsLoading { get; set; }
protected override void OnInitialized()
{
this.ResultContainerStateManager.OnLoadStart += () => this.IsLoading = true;
this.ResultContainerStateManager.OnLoadFinish += () => this.IsLoading = false;
base.OnInitializer();
}
}
public partial class ResultContainer
{
[Inject]
public ResultContainerStateManager ResultContainerStateManager { get; set; }
private bool IsLoading { get; set; }
protected override async Task OnParametersSetAsync()
{
<code>
if (shouldLoadData)
{
this.ResultContainerStateManager.NotifyLoadStart();
<more code>
this.ResultContainerStateManager.NotifyLoadFinish();
}
await base.OnParametersSetAsync();
}
}
public partial class ProgressBar
{
[Parameter]
public bool IsLoading { get; set; }
}
Where the IsLoading parameter from the progress bar is set from the IsLoading property from HeaderComponent, like
<div id="headerComponent">
<More html here>
<ProgressBar IsLoading="#this.IsLoading" />
</div>
I don't think it matters, but the progress bar itself uses the MatProgress component, like so:
#if (this.IsLoading)
{
<MatProgress Indeterminate="true" />
}
else
{
<Other html code>
}
The problem is that the progress bar starts when the ResultContainer executes the NotifyLoadStart() method, but it doesn't stop when the NotifyLoadFinish() method is executed.
I can see when debugging that the IsLoading property of the HeaderComponent is set back to false after the NotifyLoadFinish() call, but it has no Effect on the UI.
What I have tried so far:
injecting the ResultContainerStateManager directly into the Progress bar
I have tried changing the envents to Func and handling at all asynchronously
I have tried adding await Task.Yield() after each Notify call
I have tried adding this.StateHasChanged() calls in the event handlers and after each Notify call (I know the latter should not change anything at all, since it is not in the same hierarchy)
None of that changed anything and I would really like to understand why.
The only success I've had was when using EventCallbacks instead of the events. But I am using events in lots of other places and they all seem to work fine.
Could somebody tell me why events seem to fail and how this can be fixed?
Try this code
public async Task OnLoadStart()
{
this.IsLoading = true;
await InvokeAsync(() => { StateHasChanged(); });
}
public async Task OnLoadFinish()
{
this.IsLoading = false;
await InvokeAsync(() => { StateHasChanged(); });
}
protected override void OnInitialized()
{
this.ResultContainerStateManager.OnLoadStart += OnLoadStart;
this.ResultContainerStateManager.OnLoadFinish += OnLoadFinish;
}
Change : public event Action OnLoadStart;
To: public event Func<Task> OnLoadStart;
And: public event Action OnLoadFinish;
Tp: public event Func<Task> OnLoadFinish;
Implement IDisposable in the HeaderComponent component:
#implements IDisposable
public void Dispose()
{
this.ResultContainerStateManager.OnLoadStart -= OnLoadStart;
this.ResultContainerStateManager.OnLoadFinish -= OnLoadFinish;
}
Start coding asynchronously wherever you can.
How communicate in my app with my ViewModel?
I have this code, sleep and resume of my app
protected override void OnSleep()
{
MessagingCenter.Send<App, string>(this, "gotosleep", "savedata");
}
in my ViewModel I subscribe to the message, but it does not work. My message is never displayed.
public MyViewModel()
{
MessagingCenter.Subscribe<App, string>(this, "gotosleep", async (obj, item) =>
{
Console.WriteLine("HERE");
});
}
You should use the MyViewModel in ContentPage, then Subscribe of MessagingCenter will work.
public MainPage()
{
InitializeComponent();
// Use model in Content Page
MyViewModel viewModel = new MyViewModel();
}
However, I find this does not work on iOS device, but works on Android.
Here is the Workaround to solve this, you can pass the ContentPage as an attribute for ViewModel when initialiation.
public MyViewModel(MainPage mainPage)
{
MessagingCenter.Subscribe<App, string>(mainPage, "gotosleep", async (obj, item) =>
{
Console.WriteLine("HERE");
await mainPage.DisplayAlert("Message received", "arg=" + item, "OK");
});
}
In ContentPage, modify as follows:
public MainPage()
{
InitializeComponent();
// Use model in Content Page
MyViewModel viewModel = new MyViewModel(this);
}
iOS effect:
Android effect:
This answer provides a solution for an observable list that will send "list updated" notifications if properties of elements of the list change.
In my case, elements (a Element class) of such observable list are complex and I don't like to implement property for each member variable. Due to this, I added into the Element class a BooleanProperty that indicates change of the class.
Element Class
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
public class Element {
// ...
private ReadOnlyBooleanWrapper changeIndicatorWrapper;
public Element() {
//...
changeIndicatorWrapper = new ReadOnlyBooleanWrapper(false);
}
public ReadOnlyBooleanProperty changeIndicatorProperty() {
return changeIndicatorWrapper.getReadOnlyProperty();
}
public void someMethod() {
// Some update
changeIndicatorWrapper.set(!changeIndicatorWrapper.get());
}
}
Observable List
ObservableList<Element> elementsObservableList = FXCollections.observableList(
new ArrayList<>(),
(Element element) -> new Observable[] { element.changeIndicatorProperty() }
);
elementsObservableList.addListener(new ListChangeListener<Element>() {
#Override
public void onChanged(Change<? extends Element> c) {
System.out.println("CHANGE");
while(c.next()) {
if (c.wasUpdated()) {
for (int i = c.getFrom(); i < c.getTo(); ++i)
System.out.println(elementsObservableList.get(i));
}
}
}
});
My question is about this approach. Repeatedly set the changeIndicatorProperty to true not fire the change event. So, I need to reverse changeIndicatorProperty value changeIndicatorWrapper.set(!changeIndicatorWrapper.get()) each time. It is strange, isn't it?
Can I force programatically the update event?
It is strange, isn't it?
No this isn't surprising. For a change to be triggered a change needs to happen. If the BooleanProperty determines no change does happen and therefore the listeners are not notified of anything, this still satisfies the contract of Property.
Actually a Property isn't needed anyways. What is needed is a Observable that notifies it's observers. You could do this by using the following class and calling invalidate:
public class SimpleObservable implements Observable {
private final List<InvalidationListener> listeners = new LinkedList<>();
#Override
public void addListener(InvalidationListener listener) {
listeners.add(listener);
}
#Override
public void removeListener(InvalidationListener listener) {
listeners.remove(listener);
}
public void invalidate() {
for (InvalidationListener listener : listeners) {
try {
listener.invalidated(this);
} catch (RuntimeException ex) {
}
}
}
}
Example:
public class Element {
protected final SimpleObservable observable = new SimpleObservable();
public Observable getObservable() {
return observable;
}
public static <T extends Element> ObservableList<T> observableArrayList() {
return FXCollections.observableArrayList(e -> new Observable[]{e.observable});
}
private void update() {
observable.invalidate();
}
public static void main(String[] args) {
ObservableList<Element> list = Element.observableArrayList();
list.addListener((ListChangeListener.Change<? extends Element> c) -> {
while (c.next()) {
if (c.wasUpdated()) {
System.out.println("update: [" + c.getFrom() + ", " + c.getTo() + ")");
}
}
});
list.addAll(new Element(), new Element(), new Element());
list.get(1).update();
}
}
I have a simple flex3 project with and mxml file (with some as inside of it) and FMSConnection.as
I have something like this
public class FMSConnection extends NetConnection
{
//this methods is called from the media server
public function Message(message:String):void
{
//how to display (add it to a textarea) this message, when this method is invoked ?
}
}
//in the mxml, after FMSConnection is created:
fmsConn.addEventListener(FMSConnection.MESSAGE_RECEIVED, onMessage);
private function onMessage(e:Event):void
{
fmsConn = FMSConnection(e.target);
textArea.text += fmsConn.lastMessage;
}
//FMSConnection
public class FMSConnection extends NetConnection
{
public static const MESSAGE_RECEIVED:String = "messageReceived";
public var lastMessage:String;
public function Message(message:String):void
{
lastMessage = message;
dispatchEvent(new Event(MESSAGE_RECEIVED));
}
}
Instead of declaring the lastMessage variable, you can dispatch a custom event and store the message in it if you want to.
//MsgEvent.as
public class MsgEvent extends Event
{
public static const MESSAGE_RECEIVED:String = "messageReceived";
public var message:String;
public function MsgEvent(message:String, type:String)
{
super(type);
this.message = message;
}
override public function clone():Event
{
return new MsgEvent(message, type);
}
}
//in the mxml, after FMSConnection is created:
fmsConn.addEventListener(MsgEvent.MESSAGE_RECEIVED, onMessage);
private function onMessage(e:MsgEvent):void
{
textArea.text += e.message;
}
//FMSConnection
public class FMSConnection extends NetConnection
{
public function Message(message:String):void
{
dispatchEvent(new MsgEvent(message, MsgEvent.MESSAGE_RECEIVED));
}
}
Overriding the clone method is not necessary in this case, but it's a good practice to follow while using custom events. If you don't override the clone method, you will get a runtime error while trying to redispatch the custom event from the event handler.