Blazor 服务器应用程序 MVVM-Pattern:通过 Startup.cs 中的服务类从 App-State 的 child 组件更改中获得通知 parent
Blazor Server App MVVM-Pattern: Get notifyed in parent from child component change of App-State via a serviceclass in Startup.cs
我在 Blazor-Server-App 上工作并尝试遵循 MVVM 模式。我的问题是,我不明白为什么在 Child-Component.
中更改 属性 后 parent 页面不是 'auto refreshed'
我不知道如何用更简短的方式展示我的问题。这就是我发布所有代码的原因。
我编码的文字排名第一
Blazor 默认计数器模板遵循 MVVM 模式并通过依赖注入作为来自 startup.cs 的作用域服务共享状态。
索引是包含 countercomponentAsChild 的 parent 页面。
我有一个 Class 在 Startup.cs (UserSessionServiceModel) 中实例化。在这个对象中。计数器的计数存储为 object 本身实例化的 date/time。
此服务class 继承了具有 NotifyPropertyChanged 事件的 MVVM BaseModel。此服务的所有属性 class 通过 getter/setter(第一个代码块)连接到 'NotifyPropertyChanged Event'。
现在我在 Counter-Component 和索引页面的 ViewModel-Classes 的构造函数中使用此 Object (UserSession...Startup)。在 Viewpages 中,我连接 'Change Event' 并触发 StateHasChanged() 以便页面刷新并显示新数据。
--------父页索引开始
---计数器Child组件--开始
Btn.Counter++
@UserSessionServiceModel.SessionCount ---->>>> 是 SHOWN/UPDATED 如果我点击 btn
---计数器Child组件--结束
@UserSessionServiceModel.SessionCount --->>> 不是 SHOWN/UPDATED 如果我点击 Child
中的 btn
--------父页索引结束
我想因为我在服务class 中实现了 NotifyPropertyChanged,所以我也可以在 parent 中收到通知。但这不会发生 parent 不会刷新。如果 Childcomp.?!
发生变化
我确实想使用 MVVM -> ViewComponent.razor -> ViewModel.cs Class(通过构造函数使用服务class obj)和依赖注入.
如果我单独实现这些东西。它有效,但我很难将两者结合起来。
我想我需要确保 parent 收到有关 'globle' 服务class UserSessionServiceModel 更改的通知。
我做错了什么?
代码结构如下:
'UserSessionServiceModel.cs' 作为 class 我在 Startup.cs 作为 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();
}
}
对于 MVVM 模式,我使用以下在每个 ViewModel
中继承的 BaseClass
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);
}
}
现在 'Counter Component' (Child) 我使用此代码:
@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;
}
}
对于我使用的上述 CounterComponent 的 ViewModel:
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();
}
}
现在我终于可以 'use' 我的 parent IndexView-page
中的那个 child 组件
@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;
}
}
此 indexView 的 ViewModel 是:
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();
}
}
感谢您的宝贵时间:)
编辑
正如评论中所问,Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSingleton<WeatherForecastService>();
services.AddScoped<UserSessionServiceModel>();
services.AddScoped<CounterVM>();
services.AddScoped<IndexVM>();
}
编辑
我发现在 IndexVM.cs 中,通过在 'SetValue' 处放置断点并单击 Btn.Count++ 不会导致到达此断点,无法到达以下代码?!我是 Blazor 的新手,无法了解发生了什么:(
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
}
}
....}
这看起来是一个关于您如何订阅事件通知的简单问题。
首先,如果您希望页面上的所有内容都正确同步,则每个顶级页面(如索引视图)应该只有一个视图模型。在您的情况下,它看起来应该是您设置的 UserSessionServiceModel
class。令人困惑的是,您随后将其注入到 IndexVM
实例中,而您不需要这样做。
为了简化您的操作,假设您使用的是 UserSessionServiceModel
。为了使其正常工作,每个组件都需要绑定到该视图模型的相同实例。您的 startup.cs
文件看起来正确,行 services.AddScoped<UserSessionServiceModel>();
。到目前为止,还不错。
接下来,对您定义的 BaseViewModel
class 和 所有 属性进行一个小改动应该通知基础和继承 classes 中的更改:您的 SetValue<T>
方法采用通用参数来定义您正在更新的值的类型。每次调用 SetValue<T>
时,您都需要为其指定值的类型。看看我下面的例子,你就会明白我在 MyString
中调用的意思。由于类型是“string”,所以需要在属性setter里面的调用中定义。我还将 setValue<T>
中的默认值 属性 从 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);
}
}
接下来,在每个将使用视图模型的组件中,您需要在顶部添加这些行,以便您可以正确访问视图模型。
@inject UserSessionServiceModel ViewModel
@using System.ComponentModel;
@implements IDisposable
然后在代码块中,您需要像这样在视图模型中正确订阅更改事件,在使用它的每个组件中(本例为索引和计数器):
@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;
}
}
现在我们可以使用您的 SessionCount
属性 视图模型作为示例来展示如何连接它。在您的索引组件中,将其添加到视图部分:
<h4>Index section</h4>
<p>Session Count: @ViewModel.SessionCount</p>
然后在您的 Counter 组件中,添加这三行。我将在标记中使用匿名方法,但您也可以将按钮绑定到代码块中的方法。
<h4>Counter section</h4>
<p>Session Count: @ViewModel.SessionCount</p>
<button @onclick="(() => ViewModel.SessionCount++)">Increment</button>
向您的索引页面添加一些计数器组件,启动应用程序,然后单击“增量”按钮。无论您单击哪个按钮,您都应该看到每个组件中的会话计数都在增加。
由于每个组件都绑定到相同的视图模型,并且依赖注入确保它是相同的实例,因此所有内容都 link 整合在一起以实现此目的。您可以对需要通知的其他属性使用相同的模式,只需记住正确订阅并在组件完成后取消订阅并将它们添加到 SetValue<T>
,您应该处于良好状态。
由于我弄乱了这个,我做了一个基于 Blazor 服务器的简单工作示例,所以我不妨将它推上去。 Github link here
您可以使用事件或动作来通知您的父页面并执行刷新。希望这能奏效!!!!
在您的子组件中
声明-> public 动作 UpdateScreenAction { get;放; }
调用等待方法后 -> UpdateScreenAction.Invoke("componentName");
在您的界面中:
动作 UpdateGradeScreenAction { get;放; }
在您的父页面中:
在构造器中 -> GradeViewModel.UpdateGradeScreenAction = OnUpdateScreenAction;
最终创建执行操作的方法 ->
private void OnUpdateScreenAction(string componentName) =>
OnPropertyChanged(组件名称);
我在 Blazor-Server-App 上工作并尝试遵循 MVVM 模式。我的问题是,我不明白为什么在 Child-Component.
中更改 属性 后 parent 页面不是 'auto refreshed'我不知道如何用更简短的方式展示我的问题。这就是我发布所有代码的原因。
我编码的文字排名第一
Blazor 默认计数器模板遵循 MVVM 模式并通过依赖注入作为来自 startup.cs 的作用域服务共享状态。
索引是包含 countercomponentAsChild 的 parent 页面。
我有一个 Class 在 Startup.cs (UserSessionServiceModel) 中实例化。在这个对象中。计数器的计数存储为 object 本身实例化的 date/time。
此服务class 继承了具有 NotifyPropertyChanged 事件的 MVVM BaseModel。此服务的所有属性 class 通过 getter/setter(第一个代码块)连接到 'NotifyPropertyChanged Event'。
现在我在 Counter-Component 和索引页面的 ViewModel-Classes 的构造函数中使用此 Object (UserSession...Startup)。在 Viewpages 中,我连接 'Change Event' 并触发 StateHasChanged() 以便页面刷新并显示新数据。
--------父页索引开始
---计数器Child组件--开始
Btn.Counter++
@UserSessionServiceModel.SessionCount ---->>>> 是 SHOWN/UPDATED 如果我点击 btn
---计数器Child组件--结束
@UserSessionServiceModel.SessionCount --->>> 不是 SHOWN/UPDATED 如果我点击 Child
中的 btn--------父页索引结束
我想因为我在服务class 中实现了 NotifyPropertyChanged,所以我也可以在 parent 中收到通知。但这不会发生 parent 不会刷新。如果 Childcomp.?!
发生变化我确实想使用 MVVM -> ViewComponent.razor -> ViewModel.cs Class(通过构造函数使用服务class obj)和依赖注入.
如果我单独实现这些东西。它有效,但我很难将两者结合起来。
我想我需要确保 parent 收到有关 'globle' 服务class UserSessionServiceModel 更改的通知。
我做错了什么?
代码结构如下: 'UserSessionServiceModel.cs' 作为 class 我在 Startup.cs 作为 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();
}
}
对于 MVVM 模式,我使用以下在每个 ViewModel
中继承的 BaseClasspublic 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);
}
}
现在 'Counter Component' (Child) 我使用此代码:
@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;
}
}
对于我使用的上述 CounterComponent 的 ViewModel:
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();
}
}
现在我终于可以 'use' 我的 parent IndexView-page
中的那个 child 组件@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;
}
}
此 indexView 的 ViewModel 是:
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();
}
}
感谢您的宝贵时间:)
编辑 正如评论中所问,Startup.cs
public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); services.AddServerSideBlazor(); services.AddSingleton<WeatherForecastService>(); services.AddScoped<UserSessionServiceModel>(); services.AddScoped<CounterVM>(); services.AddScoped<IndexVM>(); }
编辑 我发现在 IndexVM.cs 中,通过在 'SetValue' 处放置断点并单击 Btn.Count++ 不会导致到达此断点,无法到达以下代码?!我是 Blazor 的新手,无法了解发生了什么:(
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 } } ....}
这看起来是一个关于您如何订阅事件通知的简单问题。
首先,如果您希望页面上的所有内容都正确同步,则每个顶级页面(如索引视图)应该只有一个视图模型。在您的情况下,它看起来应该是您设置的 UserSessionServiceModel
class。令人困惑的是,您随后将其注入到 IndexVM
实例中,而您不需要这样做。
为了简化您的操作,假设您使用的是 UserSessionServiceModel
。为了使其正常工作,每个组件都需要绑定到该视图模型的相同实例。您的 startup.cs
文件看起来正确,行 services.AddScoped<UserSessionServiceModel>();
。到目前为止,还不错。
接下来,对您定义的 BaseViewModel
class 和 所有 属性进行一个小改动应该通知基础和继承 classes 中的更改:您的 SetValue<T>
方法采用通用参数来定义您正在更新的值的类型。每次调用 SetValue<T>
时,您都需要为其指定值的类型。看看我下面的例子,你就会明白我在 MyString
中调用的意思。由于类型是“string”,所以需要在属性setter里面的调用中定义。我还将 setValue<T>
中的默认值 属性 从 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);
}
}
接下来,在每个将使用视图模型的组件中,您需要在顶部添加这些行,以便您可以正确访问视图模型。
@inject UserSessionServiceModel ViewModel
@using System.ComponentModel;
@implements IDisposable
然后在代码块中,您需要像这样在视图模型中正确订阅更改事件,在使用它的每个组件中(本例为索引和计数器):
@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;
}
}
现在我们可以使用您的 SessionCount
属性 视图模型作为示例来展示如何连接它。在您的索引组件中,将其添加到视图部分:
<h4>Index section</h4>
<p>Session Count: @ViewModel.SessionCount</p>
然后在您的 Counter 组件中,添加这三行。我将在标记中使用匿名方法,但您也可以将按钮绑定到代码块中的方法。
<h4>Counter section</h4>
<p>Session Count: @ViewModel.SessionCount</p>
<button @onclick="(() => ViewModel.SessionCount++)">Increment</button>
向您的索引页面添加一些计数器组件,启动应用程序,然后单击“增量”按钮。无论您单击哪个按钮,您都应该看到每个组件中的会话计数都在增加。
由于每个组件都绑定到相同的视图模型,并且依赖注入确保它是相同的实例,因此所有内容都 link 整合在一起以实现此目的。您可以对需要通知的其他属性使用相同的模式,只需记住正确订阅并在组件完成后取消订阅并将它们添加到 SetValue<T>
,您应该处于良好状态。
由于我弄乱了这个,我做了一个基于 Blazor 服务器的简单工作示例,所以我不妨将它推上去。 Github link here
您可以使用事件或动作来通知您的父页面并执行刷新。希望这能奏效!!!!
在您的子组件中
声明-> public 动作 UpdateScreenAction { get;放; }
调用等待方法后 -> UpdateScreenAction.Invoke("componentName");
在您的界面中: 动作 UpdateGradeScreenAction { get;放; }
在您的父页面中: 在构造器中 -> GradeViewModel.UpdateGradeScreenAction = OnUpdateScreenAction;
最终创建执行操作的方法 ->
private void OnUpdateScreenAction(string componentName) => OnPropertyChanged(组件名称);