如何在 属性 模型更改时更新 ShellView 中的 TextBlock

How to update a TextBlock in ShellView when property changes in Model

我在使用 LoggedInUserProfile 的 FirstName 属性 更新 ShellView TextBlock 时遇到问题,LoggedInUserProfile 在用户登录后创建为单例。

我有一个 UserProfileView,它可以绑定和更新,但 ShellView(包含 UserProfileView)没有。如果我放置断点,我可以看到 LoggedInUserProfile 有正确的数据。

这是我的第一个 WPF 应用程序,我花了一个星期 运行 自己在圈子里,试图找出我做错了什么,但知道有用,因此我正在寻求指导。

我不知道,但我怀疑我没有正确处理事件、未正确绑定或对 DI 做错了什么。

下面我提供了我认为是主要组件的代码。

我想要发生的是登录用户的名字在用户登录后以及在 UserProfileView 中显示在 ShellView 的 TextBlock 中。

如果您能提供任何帮助,为我指明正确的方向,我们将不胜感激。我已经包括了我认为是下面的主要组成部分。

ShellViewModel

using Caliburn.Micro;
using CRMDesktopUI.EventModels;
using CRMDesktopUI.Library.Models;
using System.Threading;
using System.Threading.Tasks;

namespace CRMDesktopUI.ViewModels
{
    public class ShellViewModel:Conductor<object>, IHandle<LogOnEvent>
    {
        private IEventAggregator _events;
        private SimpleContainer _container;
        private LoginViewModel _loginVM;
        private UserProfileViewModel _userProfileVM;
        private ILoggedInUserModel _loggedInUserModel;
        
        public ShellViewModel(LoginViewModel loginVM, IEventAggregator events,ILoggedInUserModel loggedInUserModel, UserProfileViewModel  userProfileVM,SimpleContainer container)
        {
            _events = events;
            _loginVM = loginVM;
            _userProfileVM = userProfileVM;
            _container = container;
            _loggedInUserModel = loggedInUserModel;
            _events.SubscribeOnUIThread(this);

            ActivateItemAsync(_loginVM);
        }

        Task IHandle<LogOnEvent>.HandleAsync(LogOnEvent message,CancellationToken cancellationToken)
        {
            _loginVM = _container.GetInstance<LoginViewModel>();
  
            ActivateItemAsync(_userProfileVM);
            
            return Task.CompletedTask;
        }

        public string FirstName
        {
            get  //This gets called before log in screen activated
            {
                if(_loggedInUserModel == null)
                {
                    return "Not logged in";
                }
                else
                {
                    return _loggedInUserModel.FirstName;
                }

            }
            set  //Set never gets called 
            {
                _loggedInUserModel.FirstName = value;
                NotifyOfPropertyChange(() => FirstName);
            }

        }
       
    }
}

ShellView.xaml

<Window x:Class="CRMDesktopUI.Views.ShellView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:CRMDesktopUI.Views"
        xmlns:viewmodels="clr-namespace:CRMDesktopUI.ViewModels"
        mc:Ignorable="d"
        Width="1250" Height="600" 
        Background="#36393F"
        ResizeMode="CanResizeWithGrip"
        AllowsTransparency="True"
        WindowStyle="None">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="200"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="25"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Border Grid.ColumnSpan="2"
                Background="#252525"
                MouseDown="Border_MouseDown">
            <Grid HorizontalAlignment="Stretch">
                <Label Content="Test App"
                         Foreground="Gray"
                         FontWeight="SemiBold"
                         FontFamily="/Fonts/#Poppins"/>
                <StackPanel HorizontalAlignment="Right"
                            Orientation="Horizontal">
                    <Button Width="20" Height="20"
                            Content=""
                            Background="Transparent"
                            BorderThickness="0"
                            Foreground="Gray"
                            FontWeight="Bold"
                            Margin=" 0 0 0 3"
                            Click="MinimiseButton_Click"/>
                    <Button Width="20" Height="20"
                            Content="□"
                            Background="Transparent"
                            BorderThickness="0"
                            Foreground="Gray"
                            FontWeight="Bold"
                            Click="MaximiseButton_Click"/>
                    <Button Width="20" Height="20"
                            Content="✕"
                            Background="Transparent"
                            BorderThickness="0"
                            Foreground="Gray"
                            FontWeight="Bold"
                            Click="CloseButton_Click"/>
                </StackPanel>
            </Grid>
        </Border>
        <Grid Background="#2F3136"
              Grid.Row="1">
            <Grid.RowDefinitions>
                <RowDefinition Height="50" />
                <RowDefinition />
                <RowDefinition Height="60" />
            </Grid.RowDefinitions>
            <Label Content="Contacts"
                   VerticalAlignment="Center"
                   FontWeight="Medium"
                   Foreground="Gray"
                   Margin="8 0 0 0" />
            <StackPanel Grid.Row="2"
                        Orientation="Horizontal"
                        Background="#292B2f">
                <Border CornerRadius="25"
                        Width="30"
                        Height="30"
                        Background="#3bff6f"
                        Margin="18 0 10 0" />
                <DockPanel VerticalAlignment="Center">
                    <TextBlock Text="First Name:"
                               Foreground="White"
                               FontWeight="Light"/>
                    <TextBlock Text="{Binding FirstName}"   <== This does not update the textblock
                               TextAlignment="Right"
                               Foreground="White"
                               FontWeight="SemiBold"
                               Margin="5 0 10 0" />               
                 </DockPanel>
            </StackPanel>
        </Grid>
        <Grid Grid.Column="1" Grid.Row="1">
            <ContentControl x:Name="ActiveItem"
                            Margin="20" />
        </Grid>
    </Grid>
</Window>

LoggedInUserModel

namespace CRMDesktopUI.Library.Models
{
    public class LoggedInUserModel:ILoggedInUserModel
    {
        public string Token { get; set; }
        public string Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string EmailAddress { get; set; }
    }
}

LoginViewModel

using Caliburn.Micro;
using CRMDesktopUI.EventModels;
using CRMDesktopUI.Library.Api;
using System;
using System.Threading.Tasks;

namespace CRMDesktopUI.ViewModels
{
    public class LoginViewModel:Screen
    {
        private string _username;
        private string _password;
        private IAPIHelper _apiHelper;
        private string _errormessage;
        private IEventAggregator _events;

        public LoginViewModel(IAPIHelper apiHelper,IEventAggregator events)
        {
            _apiHelper = apiHelper;
            _events = events;
        }

        public string ErrorMessage {
            get {
                return _errormessage;
            }
            set {
                _errormessage = value;
                NotifyOfPropertyChange(() => IsErrorVisible);
                NotifyOfPropertyChange(() => ErrorMessage);

            }
        }

        public string UserName {
            get {
                return _username;
            }
            set {
                _username = value;
                NotifyOfPropertyChange(() => UserName);
                NotifyOfPropertyChange(() => CanLogIn);
            }
        }

        public string Password {
            get {
                return _password;
            }
            set {
                _password = value;
                NotifyOfPropertyChange(() => Password);
                NotifyOfPropertyChange(() => CanLogIn);
            }
        }

        public bool CanLogIn {
            get {
                bool output = false;
                if(UserName?.Length > 0 && Password?.Length > 0)
                {
                    output = true;
                }
                return output;
            }
        }

        public bool IsErrorVisible {
            get {
                bool output = false;
                if(ErrorMessage?.Length > 0)
                {
                    output = true;
                }
                return output;
            }
        }

        public async Task LogIn()
        {
            try
            {
                ErrorMessage = "";
                var result = await _apiHelper.Authenticate(UserName,Password);
                //get more information about the logged in user
                await _apiHelper.GetLogedInUserInfo(result.Access_Token);
                await _events.PublishOnUIThreadAsync(new LogOnEvent());
            }
            catch(Exception ex)
            {
                ErrorMessage = ex.Message;
            }
        }
    }
}

UserProfileViewModel

using Caliburn.Micro;
using CRMDesktopUI.Library.Models;

namespace CRMDesktopUI.ViewModels
{
    public class UserProfileViewModel : Screen
    {
        ILoggedInUserModel _loggedInUserModel;

        public UserProfileViewModel(ILoggedInUserModel loggedInUserModel)
        {
            _loggedInUserModel = loggedInUserModel;
        }

        public string FirstName
        {
            get  //This gets called after user logs in
            {
                return _loggedInUserModel.FirstName;
            }
            set  //This never gets called
            {
                _loggedInUserModel.FirstName = value;
                NotifyOfPropertyChange(() => FirstName);
            }

        }

    }
}

UserProfileView

<UserControl x:Class="CRMDesktopUI.Views.UserProfileView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:CRMDesktopUI.Views"
             xmlns:viewmodels="clr-namespace:CRMDesktopUI.ViewModels"
             d:DataContext="{d:DesignInstance Type=viewmodels:UserProfileViewModel}"
             mc:Ignorable="d"
             Width="800" Height="450">
    <StackPanel>
        <TextBlock Text="User Profile"
                   Foreground="White"
                   FontSize="28"
                   HorizontalAlignment="Left"
                   Margin="0 0 0 20"/>
        <StackPanel Orientation="Horizontal">
            <Border Width="800"
                    Height="200">
                <Border.Background>
                    <LinearGradientBrush StartPoint="0,0"
                                        EndPoint="1,2">
                        <GradientStop Color="#5bc3ff"
                                      Offset="0.0"/>
                        <GradientStop Color="#3aa0ff"
                                      Offset="1"/>
                    </LinearGradientBrush>
                </Border.Background>
                <Border.Clip>
                    <RectangleGeometry RadiusX="10"
                                       RadiusY="10"
                                       Rect="0 0 800 200"/>
                </Border.Clip>
                <Grid>
                    <StackPanel>
                        <TextBlock Text="{Binding FirstName}"
                               Foreground="White"
                               FontSize="28"
                               Margin="20 10 10 0"/>
                    </StackPanel>
                    <Image Width="150"
                           Height="180"
                           Source="/Images/822739_user_512x512.png" 
                           HorizontalAlignment="Right"
                           VerticalAlignment="Bottom"
                           Margin="0,0,-39,-31"
                           RenderTransformOrigin="0.804,0.953">
                        <Image.RenderTransform>
                            <TransformGroup>
                                <ScaleTransform/>
                                <SkewTransform/>
                                <RotateTransform Angle="0"/>
                                <TranslateTransform/>
                            </TransformGroup>
                        </Image.RenderTransform>
                    </Image>
                </Grid>
            </Border>
        </StackPanel>
    </StackPanel>
</UserControl>

HelperClass

using CRMDesktopUI.Library.Models;
using CRMDesktopUI.Models;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace CRMDesktopUI.Library.Api
{
    public class APIHelper:IAPIHelper
    {
        private HttpClient apiClient;
        private ILoggedInUserModel _loggedInUser;
        
        public APIHelper(ILoggedInUserModel loggedInUser)
        {
            InitialiseClient();
            _loggedInUser = loggedInUser;
        }

        private void InitialiseClient()
        {
            string api = ConfigurationManager.AppSettings["api"];

            apiClient = new HttpClient();
            apiClient.BaseAddress = new Uri(api);
            apiClient.DefaultRequestHeaders.Accept.Clear();
            apiClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        }

        public async Task<AuthenticatedUser> Authenticate(string username,string password)
        {
            var data = new FormUrlEncodedContent(new[]
            {
                new KeyValuePair<string, string>("grant_type", "password"),
                new KeyValuePair<string, string>("username", username),
                new KeyValuePair<string, string>("password", password)
            });

            using(var response = await apiClient.PostAsync("/Token",data))
            {
                if(response.IsSuccessStatusCode)
                {
                    var result = await response.Content.ReadAsAsync<AuthenticatedUser>();
                    return result;
                }
                else
                {
                    throw new Exception(response.ReasonPhrase);
                }
            }
        }

        public async Task GetLogedInUserInfo(string token)
        {
            apiClient.DefaultRequestHeaders.Clear();
            apiClient.DefaultRequestHeaders.Accept.Clear();
            apiClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            apiClient.DefaultRequestHeaders.Add("Authorization",$"Bearer {token}");

            using(var response = await apiClient.GetAsync("/Api/User"))
            {
                if(response.IsSuccessStatusCode)
                {
                    var result = await response.Content.ReadAsAsync<LoggedInUserModel>();
                    _loggedInUser.Token = token.ToString();
                    _loggedInUser.Id = result.Id;
                    _loggedInUser.FirstName = result.FirstName;
                    _loggedInUser.LastName = result.LastName;
                    _loggedInUser.EmailAddress = result.EmailAddress;
                    
                }
                else
                {
                    throw new Exception(response.ReasonPhrase);
                }
            }
        }
    }
}

引导程序

using Caliburn.Micro;
using CRMDesktopUI.Helpers;
using CRMDesktopUI.Library.Api;
using CRMDesktopUI.Library.Models;
using CRMDesktopUI.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;

namespace CRMDesktopUI
{
    public class Bootstrapper:BootstrapperBase
    {
        private SimpleContainer _container = new SimpleContainer();

        public Bootstrapper()
        {
            Initialize();

            ConventionManager.AddElementConvention<PasswordBox>(
            PasswordBoxHelper.BoundPasswordProperty,
            "Password",
            "PasswordChanged");
        }
        protected override void Configure()
        {
            _container.Instance(_container);

            _container
                .Singleton<IWindowManager,WindowManager>()
                .Singleton<IEventAggregator,EventAggregator>()
                .Singleton<ILoggedInUserModel,LoggedInUserModel>()
                .Singleton<IAPIHelper, APIHelper>();

            GetType().Assembly.GetTypes()
                .Where(type => type.IsClass)
                .Where(type => type.Name.EndsWith("ViewModel"))
                .ToList()
                .ForEach(viewModelType => _container.RegisterPerRequest(viewModelType,viewModelType.ToString(),viewModelType));
        }

        protected override void OnStartup(object sender,StartupEventArgs e)
        {
            DisplayRootViewFor<MainViewModel>();
        }   

        protected override object GetInstance(Type service,string key)
        {
            return _container.GetInstance(service,key);
        }

        protected override IEnumerable<object> GetAllInstances(Type service)
        {
            return _container.GetAllInstances(service);
        }

        protected override void BuildUp(object instance)
        {
            _container.BuildUp(instance);
        }

        protected override IEnumerable<Assembly> SelectAssemblies()
        {
            return new[] { Assembly.GetExecutingAssembly() };
        }

       
    }
}

成功登录后,您需要通知 FirstName 属性 已更改。

Task IHandle<LogOnEvent>.HandleAsync(LogOnEvent message,CancellationToken cancellationToken)
{
  _loginVM = _container.GetInstance<LoginViewModel>(); // Not sure why you do this.
  ActivateItemAsync(_userProfileVM);

  // Since User has logged now, you need to notify change in FirstName

  NotifyOfPropertyChange(nameof(FirstName));
  return Task.CompletedTask;
}

这将确保 ShellView 知道 FirstName 属性 已更改。

或者您可以订阅 LoginViewModel 的 PropertyNotifyChanges 并过滤掉 FirstName 的更改,但是,由于您在登录成功后只需要名字,因此 LogOnEvent 可能更合适。

另请注意,您可以将 FirstName 属性 设置为只读,因为它很可能不会被视图编辑。

public string FirstName => _loggedInUserModel.FirstName;