Blazor Server App MVVM-Pattern:通过 Startup.cs 中的服务类从 App-State 的子组件更改中获取父级通知

Posted

技术标签:

【中文标题】Blazor Server App MVVM-Pattern:通过 Startup.cs 中的服务类从 App-State 的子组件更改中获取父级通知【英文标题】:Blazor Server App MVVM-Pattern: Get notifyed in parent from child component change of App-State via a serviceclass in Startup.cs 【发布时间】:2021-06-28 12:32:34 【问题描述】:

我在 Blazor-Server-App 上工作并尝试遵循 MVVM 模式。我的问题是,我不明白为什么在子组件中更改属性后父页面没有“自动刷新”。

我不知道如何以更短的方式展示我的问题。这就是我发布所有代码的原因。

首先我写的代码

blazor 默认计数器模板遵循 MVVM 模式,并通过依赖注入作为 startup.cs 中的作用域服务共享状态。

Index 是包含 countercomponentAsChild 的父页面。

我有一个在 Startup.cs (UserSessionServiceModel) 中实例化的类。在这个对象中。计数器的计数与对象本身实例化的日期/时间一样存储。

此服务类继承了具有 NotifyPropertyChanged 事件的 MVVM BaseModel。此服务类的所有属性都通过 getter/setter(第一个代码块)连接到“NotifyPropertyChanged 事件”。

现在我在 Counter-Component 和 Index 页面的 ViewModel-Classes 的构造函数中使用此对象 (UserSession...Startup)。在 Viewpages 中,我连接了“更改事件”并触发 StateHasChanged(),因此页面应该刷新并显示新数据。

-------父页面索引开始

---计数器子组件--开始

Btn.Counter++

@UserSessionServiceModel.SessionCount ---->>>> 如果我单击 btn,则会显示/更新

---计数器子组件--结束

@UserSessionServiceModel.SessionCount --->>> 如果我单击 Child 中的 btn,则不会显示/更新

-------父页面索引结束

我想因为我在 Serviceclass 中实现了 NotifyPropertyChanged,所以我也可以在父级中得到通知。但这不会发生,父母不会刷新。如果 Childcomp 发生变化。?!

我确实想使用 MVVM -> ViewComponent.razor -> ViewModel.cs 类(通过构造函数使用服务类 obj)和依赖注入。

如果我分别意识到这些东西。它有效,但我很难将两者结合起来。

我认为我需要确保通知父级“全球”服务类 UserSessionServiceModel 的更改。

我做错了什么?

代码结构如下: 'UserSessionServiceModel.cs' 作为我在 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);
    

现在对于“计数器组件”(子级),我使用以下代码:

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

现在我终于可以在我的父 IndexView 页面中“使用”那个子组件了

@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
         
     
     ....
    

【问题讨论】:

您能否展示您的 startup.cs 类以显示服务是如何注册以进行依赖注入的?您在这里所拥有的似乎是在某处实例化服务模型的离散实例,这可能是问题所在。 我在原帖末尾添加了 Startup.cs 如果我从 indexpage 更改为 weatherpage,然后再返回 indexpage。 UserSessionServiceModel 中的数据在 Child 和 Parent 中正确显示。而且 createDateTime 仍然与第一次网页浏览/启动时相同,它没有像我看到的那样重新创建。如果你是这个意思? 【参考方案1】:

这看起来是一个关于您如何订阅事件通知的简单问题。

首先,如果您希望页面上的所有内容正确同步,则每个***页面(如索引视图)应该只有一个视图模型。在您的情况下,它看起来应该是您设置的 UserSessionServiceModel 类。但令人困惑的是,您随后将其注入到 IndexVM 实例中,而您不需要这样做。

所以为了简化这一点,假设UserSessionServiceModel 是您正在使用的。为了使其正常工作,每个组件都需要绑定到该视图模型的同一实例。您的startup.cs 文件与services.AddScoped&lt;UserSessionServiceModel&gt;(); 行看起来是正确的。到目前为止,一切顺利。

接下来,您的 BaseViewModel 类和所有您定义的应该通知更改的属性在基类和继承类中进行小幅更改:您的 @ 987654328@ 方法采用通用参数来定义您正在更新的值的类型。每次调用SetValue&lt;T&gt; 时,都需要为其指定值的类型。看看我下面的例子,你就会明白我在MyString 中调用的意思。由于类型是“字符串”,因此需要在属性设置器内部的调用中定义。我还将setValue&lt;T&gt; 中的默认属性从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 属性作为示例来展示如何连接。在您的 Index 组件中,将其添加到视图部分:

<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>

向您的索引页面添加一些计数器组件,启动应用程序,然后单击“增量”按钮。无论您单击了哪个按钮,您都应该看到每个组件中的会话计数都在增加。

由于每个组件都绑定到同一个视图模型,并且依赖注入确保它是同一个实例,因此所有组件都链接在一起以使其工作。您可以对需要通知的其他属性使用相同的模式,只需记住正确订阅并在组件完成后取消订阅并将它们类型添加到SetValue&lt;T&gt;,您应该处于良好状态。

当我搞砸这个时,我做了一个基于 Blazor 服务器的简单工作示例,所以我不妨把它推上去。 Github链接here

【讨论】:

我快速浏览了你的 Git 存储库。我需要一点时间来解决这个问题......

以上是关于Blazor Server App MVVM-Pattern:通过 Startup.cs 中的服务类从 App-State 的子组件更改中获取父级通知的主要内容,如果未能解决你的问题,请参考以下文章

Blazor WASM 路由发布请求到 index.html

Blazor Server 和 WebAssembly 应用程序入门指南

一文说通Blazor for Server-Side的项目结构

Blazor Server Webapi 不适用于邮递员

声明的状态管理如何在 Blazor Server 中工作?

Blazor - app.UseIdentityServer();使用 .pfx 密钥文件 - 解析数字时遇到意外字符