'Blazor Server App MVVM-Pattern: Get notifyed in parent from child component change of App-State via a serviceclass in Startup.cs
I work on a Blazor-Server-App and try to follow MVVM pattern. My problem is, that I don't understand why a parentpage is not 'auto refreshed' after a property is changed in a Child-Component.
I didn't know how to show my problem in a shorter way. That is why I posted all the code.
First in Words what I coded
The blazor default counter template following MVVM pattern and share state via dependency injection as a scopedservice from startup.cs.
Index is parentpage with countercomponentAsChild in it.
I have a Class that is instantiated in Startup.cs (UserSessionServiceModel). In this obj. the count of the counter is stored as is the date/time of the instantiation of the object itself.
This Serviceclass inherites the MVVM BaseModel that has NotifyPropertyChanged Event. All Propertys of this serviceclass are connected to 'NotifyPropertyChanged Event' via getter/setter (first code block).
Now I use this Object (UserSession...Startup) in the Constructor of the ViewModel-Classes from Counter-Component and Index page. In the Viewpages to these I connect the 'Change Event' and fire a StateHasChanged() so the page should refresh and the new data should be shown.
-------ParentPage Index Start
---Counter Child Compmonent--Start
Btn.Counter++
@UserSessionServiceModel.SessionCount ---->>>> is SHOWN/UPDATED if I click the btn
---Counter Child Compmonent--End
@UserSessionServiceModel.SessionCount --->>> is NOT SHOWN/UPDATED if I click the btn in the Child
-------ParentPage Index End
I thought because I have the NotifyPropertyChanged implemented in the Serviceclass that I would be possible to get notifyed in the parent as well. But that doesn't happen the parent doesn't get refreshed. If changes happen in the Childcomp.?!
I do want to use MVVM -> ViewComponent.razor -> ViewModel.cs Class (with the use of the serviceclass obj via Constructor) and the Dependence Injection.
If I realize the stuff seperately. It works but I struggle to combine both.
I think I need to make sure that the parent is Notifyed of change in the 'globle' serviceclass UserSessionServiceModel.
What am I doing wrong?
The codestructur is as follows: 'UserSessionServiceModel.cs' as a class that I start in Startup.cs as a ScopedService
public class UserSessionServiceModel : BaseViewModel
{
public int _sessionCount;
public int SessionCount
{
get => _sessionCount;
set { SetValue(ref _sessionCount, value); }
}
public string _datumCreateString;
public string DatumCreateString
{
get => _datumCreateString;
set { SetValue(ref _datumCreateString, value); }
}
public string _datumEditString;
public string DatumEditString
{
get => _datumEditString;
set { SetValue(ref _datumEditString, value); }
}
public UserSessionServiceModel()
{
SessionCount = -2;
DatumCreateString = DateTime.Now.ToString();
}
}
for the MVVM pattern I use the following BaseClass that is inherieted in every ViewModel
public abstract class BaseViewModel : INotifyPropertyChanged
{
private bool isBusy = false;
public bool IsBusy
{
get => isBusy;
set
{
SetValue(ref isBusy, value);
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected void SetValue<T>(ref T backingFiled, T value, [CallerMemberName] string
propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(backingFiled, value)) return;
backingFiled = value;
OnPropertyChanged(propertyName);
}
}
Now for the 'Counter Component' (Child) I use this code:
@inject CounterVM ViewModel
@using System.ComponentModel;
@implements IDisposable
<hr>
<h5>CHILD</h5>
<h5>CounterVComp</h5>
<br />
<p>Current count: @ViewModel.UserSessionServiceModel.SessionCount</p>
<button class="btn btn-primary" @onclick="ViewModel.IncrementCount">Click me</button>
<p>CREATE: @ViewModel.UserSessionServiceModel.DatumCreateString</p>
<p>EDIT: @ViewModel.UserSessionServiceModel.DatumEditString</p>
<h5>CHILD</h5>
<hr>
@code {
protected override async Task OnInitializedAsync()
{
ViewModel.PropertyChanged += async (sender, e) =>
{
await InvokeAsync(() =>
{
StateHasChanged();
});
};
await base.OnInitializedAsync();
}
async void OnPropertyChangedHandler(object sender, PropertyChangedEventArgs e)
{
await InvokeAsync(() =>
{
StateHasChanged();
});
}
public void Dispose()
{
ViewModel.PropertyChanged -= OnPropertyChangedHandler;
}
}
for the ViewModel to the above CounterComponent I use:
public class CounterVM : BaseViewModel
{
//inject the Service in the class
public CounterVM(UserSessionServiceModel userSessionServiceModel)
{
UserSessionServiceModel = userSessionServiceModel;
}
private UserSessionServiceModel _userSessionServiceModel;
public UserSessionServiceModel UserSessionServiceModel
{
get => _userSessionServiceModel;
set
{
SetValue(ref _userSessionServiceModel, value);
}
}
public async Task IncrementCount()
{
UserSessionServiceModel.SessionCount++;
UserSessionServiceModel.DatumEditString = DateTime.Now.ToString();
}
}
Now atlast I can 'use' that child component in my parent IndexView-page
@page "/"
@inject IndexVM ViewModel
@using System.ComponentModel;
@implements IDisposable
<CounterVComp ></CounterVComp>
<h5>Parent</h5>
<p>@ViewModel.UserSessionServiceModel.SessionCount</p>
<p>CREATE: @ViewModel.UserSessionServiceModel.DatumCreateString</p>
<p>EDIT: @ViewModel.UserSessionServiceModel.DatumEditString</p>
<p>------------</p>
<p>@ViewModel.TestString</p>
<button class="btn btn-primary" @onclick="ViewModel.ChangeTestString">Change to Scotty</button>
@code {
protected override async Task OnInitializedAsync()
{
ViewModel.PropertyChanged += async (sender, e) =>
{
await InvokeAsync(() =>
{
StateHasChanged();
});
};
await base.OnInitializedAsync();
}
async void OnPropertyChangedHandler(object sender, PropertyChangedEventArgs e)
{
await InvokeAsync(() =>
{
StateHasChanged();
});
}
public void Dispose()
{
ViewModel.PropertyChanged -= OnPropertyChangedHandler;
}
}
The ViewModel for this indexView is:
public class CounterVM : BaseViewModel
{
public CounterVM(UserSessionServiceModel userSessionServiceModel)
{
UserSessionServiceModel = userSessionServiceModel;
}
private UserSessionServiceModel _userSessionServiceModel;
public UserSessionServiceModel UserSessionServiceModel
{
get => _userSessionServiceModel;
set
{
SetValue(ref _userSessionServiceModel, value);
}
}
public async Task IncrementCount()
{
UserSessionServiceModel.SessionCount++;
UserSessionServiceModel.DatumEditString = DateTime.Now.ToString();
}
}
Thank you for your time :)
EDIT Here as asked in the comment the Startup.cs
public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); services.AddServerSideBlazor(); services.AddSingleton<WeatherForecastService>(); services.AddScoped<UserSessionServiceModel>(); services.AddScoped<CounterVM>(); services.AddScoped<IndexVM>(); }
EDIT I found out that in IndexVM.cs the following code never gets reached by putting a breakpoint at 'SetValue' and clicking Btn.Count++ didn't result in reaching this break point?! I'am to new to Blazor to understand what has happened :(
public class IndexVM : BaseViewModel { public IndexVM(UserSessionServiceModel userSessionServiceModel) { _userSessionServiceModel = userSessionServiceModel; } private UserSessionServiceModel _userSessionServiceModel; public UserSessionServiceModel UserSessionServiceModel { get => _userSessionServiceModel; set { SetValue(ref _userSessionServiceModel, value);// BREAKPOINT not reached } } ....}
Solution 1:[1]
This looks to be a simple issue of how you are subscribing to the event notifications.
First off, you should only have one view model per top level page (like the index view) if you want everything on the page to synchronize properly. In your case it looks like it should be the the UserSessionServiceModel
class that you set up. The confusing piece though is that you are then injecting that into an IndexVM
instance, and you shouldn't need to do that.
So to simplify this for you, let's say that UserSessionServiceModel
is what you are using. In order for this to work correctly, each component needs to be bound to the same instance of that view model. Your startup.cs
file looks correct with the line services.AddScoped<UserSessionServiceModel>();
. So far, so good.
Next, a small change in your BaseViewModel
class and all of the properties you define that should notify of changes, in both base and inherited classes: Your SetValue<T>
method takes a generic parameter to define the type of the value you are updating. Each time you call SetValue<T>
you need to give it the type of the value. Take a look at my example below and you'll see what I mean with the call in MyString
. Since the type is "string", it needs to be defined in the call inside the property setter. I also changed the default property in setValue<T>
to "" from null.
public abstract class BaseViewModel : INotifyPropertyChanged
private string _mystring;
public string MyString
{
get { return _myString; }
set
{
// Note the use of the generic string argument here
SetValue<string>(ref _myString, value);
}
}
public event PropertyChangedEventHandler PropertyChanged;
// changed default propertyName parameter from null to "", both locations
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected void SetValue<T>(
ref T backingFiled,
T value,
[CallerMemberName] string propertyName = "")
{
if (EqualityComparer<T>.Default.Equals(backingFiled, value)) return;
backingFiled = value;
OnPropertyChanged(propertyName);
}
}
Next, in every component that will use the view model, you will want these lines added at the top so that you can gain access to the view model correctly.
@inject UserSessionServiceModel ViewModel
@using System.ComponentModel;
@implements IDisposable
and then in the code block you need to subscribe properly to the change event in the view model like this, in every component that uses it (Index and counter for this example):
@code {
protected override async Task OnInitializedAsync()
{
// You need to subscribe your method to the handler here,
// not an anonymous method like you had it before
ViewModel.PropertyChanged += OnPropertyChangedHandler;
// This call isn't needed, in base it's just a stub
await base.OnInitializedAsync();
}
async void OnPropertyChangedHandler(object sender, PropertyChangedEventArgs e)
{
await InvokeAsync(() =>
{
StateHasChanged();
});
}
public void Dispose()
{
ViewModel.PropertyChanged -= OnPropertyChangedHandler;
}
}
Now we can use your SessionCount
property of the view model as an example to show how this can be wire up. In your Index component, add this into the view portion:
<h4>Index section</h4>
<p>Session Count: @ViewModel.SessionCount</p>
Then in your Counter component, add these three lines in. I'll use an anonymous method in the markup, but you could also bind the button to a method in your code block.
<h4>Counter section</h4>
<p>Session Count: @ViewModel.SessionCount</p>
<button @onclick="(() => ViewModel.SessionCount++)">Increment</button>
Add a few counter components to your index page, fire the app up, and click the "Increment" buttons. You should see your session count incrementing up in every component regardless of which button you clicked.
Since every component is bound to the same view model, and dependency injection is making sure it's the same instance, everything is linked together to make this work. You can use the same pattern for your other properties that need to notify, just remember to subscribe correctly and unsubscribe when components are done and to add they type to SetValue<T>
and you should be in good shape.
As I messed around with this I made a simple working example based on Blazor server, so I might as well push it up. Github link here
Solution 2:[2]
You can use an event or action to notify to you parent page and execute the refresh. Hope this could work!!!!
In your child component
declare-> public Action UpdateScreenAction { get; set; }
after you call your await method -> UpdateScreenAction.Invoke("componentName");
In you interface: Action UpdateGradeScreenAction { get; set; }
In you parent page: In the contructor -> GradeViewModel.UpdateGradeScreenAction = OnUpdateScreenAction;
Final create the method to execute the action ->
private void OnUpdateScreenAction(string componentName) => OnPropertyChanged(componentName);
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Nik P |
Solution 2 | Angel |