Blazor state not updatting between child components when using events - .net-core

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.

Related

Blazor Wasm notifiy parent component of state change

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();
}
}

List parameter on child component not updating

I have a child component for filtering a search (DropdownFilter) which takes an input of a list of suggestions and a function to update that list.
For some reason DropdownFilter.Suggestions isn't being updated after it is initially set and I don't know how to update it again. Any information about how to update the property after it is initially bound would be great!
DropdownFilter.razor:
<input id="search" #onfocus="SearchFocused" #onblur="SearchUnfocused" #oninput="UpdateSearchText" />
#foreach (var suggestion in Suggestions)
{
<p>#suggestion</p>
}
#code {
[Parameter]
public Action<string> SearchFieldChanged { get; set; }
//[Parameter]
//public RenderFragment<TSuggestion> SuggestionTemplate { get; set; }
[Parameter]
public List<string> Suggestions { get; set; }
private bool searchFocus = false;
private void SearchFocused(FocusEventArgs args) {
searchFocus = true;
//UpdateSearchText();
}
private void SearchUnfocused(FocusEventArgs args) => searchFocus = false;
private void UpdateSearchText(ChangeEventArgs args)
{
SearchFieldChanged.Invoke((string)args.Value);
}
public void Refresh() {
StateHasChanged();
}
}
Index.razor:
#page "/example"
<div class="container-fluid dropdown-holder">
<DropdownFilter #ref="dropdown" Suggestions="#maskResults" SearchFieldChanged="UpdateSearchResults" />
</div>
#code {
DropdownFilter dropdown;
public class MaskResult {
public string name;
}
static readonly string[] allMasks = {
"Electric",
"Water",
"Ground",
"Fire",
"Bug"
};
public List<string> maskResults = allMasks.ToList();
private void UpdateSearchResults(string search)
{
search = search.ToLower();
maskResults = allMasks.Where((mask) =>
{
return mask.ToLower().StartsWith(search);
}).ToList();
dropdown.Refresh();
}
}
I think that you are trying to create a Datalist, please check this answer:"
datalist How to bind selected item to object
If you add a StateHasChanged() call just here it should work:
private void UpdateSearchResults(string search)
{
search = search.ToLower();
maskResults = allMasks.Where((mask) =>
{
return mask.ToLower().StartsWith(search);
}).ToList();
StateHasChanged(); // Add this line
dropdown.Refresh();
}
As I understand, if you update manually a Parameter of a component, there are some cases where Blazor does not get automatically the info that it needs to update its components. So if you call StateHasChanged, it will reevaluate all Parameters of the childreen of the component where you do the call.
I'll let someone correct me if I am wrong.
Thanks and good luck :)

Xamarin forms Back Button Navigation

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();
}

How can I communicate in my app with ViewModel?

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:

Caliburn Action not firing

<ItemsControl DockPanel.Dock="Right" x:Name="Actions">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button x:Name="Action"
HorizontalAlignment="Right"
Content="{Binding Label}"
Margin="3" Width="30"></Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
The above view binds with this viewmodel
public class DeploymentInputViewModel<T> : PropertyChangedBase
{
public BindableCollection<InputActionViewModel> Actions {get;set;}
}
I see my buttons. But when clicking it nothing happen.
The viewModels for InputActionViewModel:
public abstract class InputActionViewModel{
public InputActionViewModel()
{
}
public virtual Task Action()
{
return Task.FromResult<object>(null);
}
public string ActionToolTip { get; set; }
public string Label { get; set; }
public object Value { get; set; }
}
and also
public class InputCertificateActionViewModel : InputActionViewModel
{
[Import]
private IShell _shell;
[Import]
private IWindowsDialogs _dialogs;
private readonly IDeploymentSettingInputViewModel vm;
public InputCertificateActionViewModel(IDeploymentSettingInputViewModel vm)
{
this.vm = vm;
Label = "...";
ActionToolTip = "Pick a Certificate";
}
public bool IsManagementCertificate {get;set;}
public bool IsDeploymentCertificate { get; set; }
public async override Task Action()
{
if(IsManagementCertificate)
{
var subs = await _shell.IdentityModel.GetEnabledSubscriptionsAsync();
foreach(var sub in subs)
{
using (ManagementClient client = CloudContext.Clients.CreateManagementClient(sub.GetCredentials()))
{
var cert = _dialogs.SelectItemDialog("Select a certificate", "Pick one", true,
(await client.ManagementCertificates.ListAsync()).Select(c =>
new SelectItem(c.Thumbprint, Encoding.Default.GetString(c.PublicKey), c, (s) => c.Thumbprint.Contains(s))).ToArray())
.Tag as ManagementCertificateListResponse.SubscriptionCertificate;
this.vm.Value = cert.Thumbprint;
}
}
}else if(IsDeploymentCertificate)
{
}
}
}
I am adding actionViewModels by inserting directly into the observable code at startup.
haveActions.Actions.Add(DI.BuildUp(new InputCertificateActionViewModel(vm)
{
IsDeploymentCertificate = certAttribute.IsDeploymentCertificate,
IsManagementCertificate = certAttribute.IsManagementCertificate,
}));
haveActions is an instance of InputCertificateActionViewModel
Couldn't fit this all in a comment:
I can't have a peek at the Caliburn.Micro at the moment, but it might be something related to calling your method Action.
At a guess though, I'd say that by convention Caliburn.Micro expects to find a method that matches the Action<T> delegate to use for it's Actions, so your public virtual Task Action() won't be located and bound.
Have a quick check by defining a new method with a compatible signature, e.g public void MyMethod() and checking to see if it's located correctly and will function.
If that is the problem, you'll probably want to have a look at the IResult and Coroutines part of the Caliburn.Micro documentation, which looks like it will help you implement your desired behaviour.

Resources