【问题标题】:C# WPF MVVM Blocking UI ThreadC# WPF MVVM 阻塞 UI 线程
【发布时间】:2019-03-27 19:15:38
【问题描述】:

我不太确定,我的问题/错误在哪里。 我将 WPF 与 MVVM 模式结合使用,而我的问题在于登录。

我的第一次尝试效果很好。我有几个窗口,每个窗口都有自己的 ViewModel。 在登录视图模型中,我运行了以下代码:

PanelMainMessage = "Verbindung zum Server wird aufgebaut";
PanelLoading = true;

_isValid = _isSupportUser = false;
string server = Environment.GetEnvironmentVariable("CidServer");
string domain = Environment.GetEnvironmentVariable("SMARTDomain");
try
{
    using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, server + "." + domain))
    {
        // validate the credentials
        PanelMainMessage = "username und passwort werden überprüft";
        _isValid = pc.ValidateCredentials(Username, _view.PasswortBox.Password);
        PanelMainMessage = "gruppe wird überprüft";
        _isSupportUser = isSupport(Username, pc);
    }
 }
 catch (Exception ex)
 {
     //errormanagement -> later
 }

 if (_isValid)
 {
     PanelLoading = false;
     if (_isSupportUser)
          _mainwindowviewmodel.switchToQuestionView(true);
     else
          _mainwindowviewmodel.switchToQuestionView(false);

  }
  else
      PanelMainMessage = "Verbindung zum Server konnte nicht hergestellt werden";

该部分连接到 Active Directory 并首先检查登录是否成功,然后检查用户是否有某个广告组(在方法 isSupport 中)

我在视图中有一个显示,就像一个进度条。当 PanelLoading 等于 true 时,它​​处于活动状态。

到目前为止一切正常。

然后我创建了一个包含内容控件的主窗口,并将我的视图更改为用户控件,因此我可以交换它们。 (目的是不为每个视图打开/创建一个新窗口)。

当我现在执行代码时,我的 GUI 会阻塞,直到执行上述部分。我尝试了几种方法...

  • 将代码 sn-p 移动到一个附加方法中并将其作为一个自己的线程启动:

    Thread t1 = new Thread(() => loginThread());
    t1.SetApartmentState(ApartmentState.STA);
    t1.Start();
    

    当我这样做时,我收到一个错误,即资源由另一个线程拥有,因此无法访问。 (调用线程无法访问此对象,因为不同的线程拥有它)

  • 然后,尝试调用登录部分,而不是附加线程;登录包含之前的代码sn -p

    Application.Current.Dispatcher.Invoke((Action)(() =>
        {
            login(); 
        }));
    

    那行不通。至少不是我的实现方式。

  • 之后,我尝试在一个线程中只运行登录 sn-p 的主要部分,然后在完成后引发一个先前注册的事件,该事件将处理内容控件的更改。这就是我在线程访问另一个线程拥有的资源时遇到错误的部分,所以我想,我可以解决这个问题。

    void HandleThreadDone(object sender, EventArgs e)
    {
        if (_isValid)
        {
            PanelLoading = false;
            _mainwindowviewmodel.switchToQuestionView(_isSupportUser);
        }
        else
            PanelMainMessage = "Verbindung zum Server konnte nicht hergestellt werden";
    }
    

    在登录方法中我会调用 ThreadDone(this, EventArgs.Empty);完成后。好吧,关于另一个线程拥有的资源,我遇到了同样的错误。

现在我在这里寻求帮助......

我知道我的代码不是最漂亮的,我至少两次打破了 mvvm 模式背后的想法。另外我对Invoke方法了解不多,但我尽力了,在stackoverflow和其他网站上搜索了一段时间(2-3小时),没有成功。

指定线程错误发生的位置:

_mainwindowviewmodel.switchToQuestionView(_isSupportUser);

which leads to the following method

public void switchToQuestionView(bool supportUser)
    {
        _view.ContentHolder.Content = new SwitchPanel(supportUser);
    }

这也是我不使用数据绑定的一种情况。我更改了 contentcontrol 的内容:

 <ContentControl Name="ContentHolder"/>

我将如何使用数据绑定来实现这一点。属性是否应该具有 ContentControl 类型?我真的找不到答案。并且通过将其更改为DataBinding,是否可以解决线程所有权的错误?

项目结构如下: 主视图是入口点,在构造函数中数据上下文设置为当时创建的主视图模型。主视图有一个内容控件,我在其中交换我的用户控件,在本例中是我的视图。

在我的 mainviewmodel 中,我在 usercontrol 登录的开头设置了 contentcontrol 的内容,这会在其构造函数中创建一个 viewmodel 并将其设置为 datacontext。

代码 sn-ps 来自我的 loginviewmodel。希望这会有所帮助。

我以为我找到了解决方法,但它仍然不起作用。我忘了,定时器在后台是如何工作的,所以也可以这样解决。

【问题讨论】:

  • 请参阅My Answer,了解关于 WPF 和 MVVM 中的 UI 线程的封送属性更改通知。它可能会对你有所帮助。
  • 您使用的是哪个 .NET 框架版本?
  • @HighCore 我知道属性更改了功能并且正在使用它。我知道,我的 contentcontrol 没有数据绑定,但这仅仅是因为在 contentcontrol 上实现数据绑定对我来说似乎很困难。这是解决我问题的唯一方法吗?
  • @YuvalItzchakov .NET 4.5
  • 旁注——放弃创建线程。上任务马车。

标签: c# wpf multithreading mvvm blocking


【解决方案1】:

问题在于 WPF 或一般的 XAML 框架不允许从其他线程修改主线程上的可视元素。为了解决这个问题,您应该区分从第二个线程更新视图的代码部分。在你的情况下,我可以看到:

_view.ContentHolder.Content = new SwitchPanel(supportUser);

改变视图。 为了解决这个问题,你可以试试这个answer。其中我使用同步上下文来进行线程之间的通信。

另一种解决方法(这可能是调度程序的错误用法)是使用调度程序将修改视图的操作“发送”到主线程。像这样的东西:

var dispatcher = Application.Current.Dispatcher;

//also could be a background worker
Thread t1 = new Thread(() => 
                          {
                               dispatcher .Invoke((Action)(() =>
                               {
                                    login();    //or any action that update the view
                               })); 
                              //loginThread();
                          });
t1.SetApartmentState(ApartmentState.STA);
t1.Start();

希望这会有所帮助...

【讨论】:

    【解决方案2】:

    一种常见的方法是实现AsyncRelayCommand(在某些教程中也称为AsyncDelegateCommand,并将其绑定到 WPF 视图。

    这是我用于演示项目的示例实现,用于熟悉 WPF、MVVM 和 DataBinding。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows.Input;
    
    public class AsyncRelayCommand : ICommand {
        protected readonly Func<Task> _asyncExecute;
        protected readonly Func<bool> _canExecute;
    
        public event EventHandler CanExecuteChanged {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
    
        public AsyncRelayCommand(Func<Task> execute)
            : this(execute, null) {
        }
    
        public AsyncRelayCommand(Func<Task> asyncExecute, Func<bool> canExecute) {
            _asyncExecute = asyncExecute;
            _canExecute = canExecute;
        }
    
        public bool CanExecute(object parameter) {
            if(_canExecute == null) {
                return true;
            }
    
            return _canExecute();
        }
    
        public async void Execute(object parameter) {
            await ExecuteAsync(parameter);
        }
    
        protected virtual async Task ExecuteAsync(object parameter) {
            await _asyncExecute();
        }
    }
    

    这是LoginViewModel

    // ViewBaseModel is a basic implementation of ViewModel and INotifyPropertyChanged interface 
    // and which implements OnPropertyChanged method to notify the UI that a property changed
    public class LoginViewModel : ViewModelBase<LoginViewModel> {
        private IAuthService authService;
        public LoginViewModel(IAuthService authService) {
            // Inject authService or your Context, whatever you use with the IoC 
            // framework of your choice, i.e. Unity
            this.authService = authService 
        }
    
        private AsyncRelayCommand loginCommand;
        public ICommand LoginCommand {
            get {
                return loginCommand ?? (loginCommand = new AsyncCommand(Login));
            }
        }
    
        private string username;
        public string Username {
            get { return this.username; }
            set {
                if(username != value) {
                    username = value;
    
                    OnPropertyChanged("Username");
                }
            }
        }
    
        private string password;
        public string Password {
            get { return this.password; }
            set {
                if(password != value) {
                    password = value;
    
                    OnPropertyChanged("Password");
                }
            }
        }
    
        private async Task Search() {
            return await Task.Run( () => {
                    // validate the credentials
                    PanelMainMessage = "username und passwort werden überprüft";
                    // for ViewModel properties you don't have to invoke/dispatch anything 
                    // Only if you interact with i.e. Observable Collections, you have to 
                    // run them on the main thread
                    _isValid = pc.ValidateCredentials(this.Username, this.Password);
                    PanelMainMessage = "gruppe wird überprüft";
                    _isSupportUser = isSupport(Username, pc);
                }                
            } );
        }
    }
    

    现在您将UsernamePassword 属性作为双向绑定绑定到您的文本字段,并将您的LoginCommand 命令绑定到您的登录按钮。

    最后但同样重要的是,ViewModelBase 的一个非常基本的实现。

    public abstract class ViewModelBase<T> : INotifyPropertyChanged {
        public event PropertyChangedEventHandler PropertyChanged;
    
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName) {
            var handler = PropertyChanged;
    
            if (handler != null) {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
    

    最后的一些说明: 正如您已经提到的,上面的代码存在几个问题。您从 ViewModel 引用视图。这几乎破坏了整个事情,如果您开始从 ViewModel 引用视图,您可以完全跳过 MVVM 并使用 WPF 的 CodeBehind。

    此外,您应该避免从您的 ViewModel 中引用其他 ViewModel,因为这会将它们紧密耦合,并使单元测试变得非常困难。

    要在视图/视图模型之间导航,通常需要实现 NavigationService。您在模型中定义 NavigationService 的接口(即INavigationService)。但是 NavigationService 的实现发生在表示层(即您的视图所在的位置/项目),因为这是您可以实现 NavigationService 的唯一位置。

    导航服务非常特定于应用程序/平台,因此需要为每个平台(Desktop、WinRT、Silverlight)实现一个新的。显示对话框消息/弹出窗口的 DialogService 也是如此。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多