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

感谢您的宝贵时间:)

  1. 编辑 正如评论中所问,Startup.cs

         public void ConfigureServices(IServiceCollection services)
     {
         services.AddRazorPages();
         services.AddServerSideBlazor();
         services.AddSingleton<WeatherForecastService>();
         services.AddScoped<UserSessionServiceModel>();
         services.AddScoped<CounterVM>();
         services.AddScoped<IndexVM>();
     }
    
  2. 编辑 我发现在 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(组件名称);