WPF MVVM阻塞UI线程

时间:2022-01-22 01:13:32

I am not quite sure, where my problem/mistake is. I am using WPF in combination with the MVVM pattern and my problem is at the login.

我不太清楚我的问题在哪里。我正在结合使用WPF和MVVM模式,我的问题是登录。

My first attempt worked fine. I had several windows, each with their own ViewModel. In the Login ViewModel I had following code running:

我的第一次尝试很成功。我有几个窗口,每个窗口都有自己的视图模型。在Login 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";

That part connects to an Active Directory and first checks if the login was succesfull and then, if the user has a certain ad group (in method isSupport)

该部分连接到一个活动目录,首先检查登录是否成功,然后,如果用户有某个ad组(在method isSupport中)

I have a display in the view, which is like a progress bar. It is active when PanelLoading equals true.

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

Until now everything worked.

直到现在一切工作。

Then I created a main window with a contentcontrol in it and changed my views to user controls, so I could swap them. (The intention was, not to open/create a new window for every view).

然后我创建了一个主窗口,其中包含一个contentcontrol,并将视图更改为user controls,以便交换它们。(目的是不为每个视图打开/创建一个新窗口)。

When I execute the code now, my GUI blocks, until said part is executed. I have tried several ways...

当我现在执行代码时,我的GUI模块,直到被执行的部分被执行。我试过几种方法……

  • Moving the code snippet into an additional method and starting it as an own thread:

    将代码片段移动到另一个方法中,并将其作为自己的线程启动:

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

    When I do it this way, I get an error that a ressource is owned by an another thread and thus cannot be accessed. (the calling thread cannot access this object because a different thread owns it)

    当我这样做时,我得到一个错误,一个ressource被另一个线程拥有,因此不能被访问。(调用线程不能访问该对象,因为另一个线程拥有该对象)

  • Then, instead of an additional thread, trying to invoke the login part; login containing the previous code snippet

    然后,不再使用额外的线程,而是尝试调用登录部分;包含先前代码片段的登录

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

    That does not work. At least not how I implemented it.

    这并不工作。至少不是我怎么实现的。

  • After that, I tried to run only the main part of the login snippet in a thread and after that finished, raising an previously registered event, which would handle the change of the content control. That is the part, where I get the error with the thread accessing a ressource owned by another thread, so I thought, I could work around that.

    之后,我尝试在线程中只运行登录代码片段的主要部分,在完成之后,抛出一个以前注册的事件,该事件将处理内容控件的更改。在这一部分,当线程访问由另一个线程拥有的ressource时,我得到了错误,因此我想,我可以解决这个问题。

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

    And in the login method I would call ThreadDone(this, EventArgs.Empty); after it finished. Well, I got the same error regarding the ressource owned by an another thread.

    在登录方法中,我将调用ThreadDone(这个,EventArgs.Empty);后完成。对于另一个线程拥有的ressource,我得到了相同的错误。

And now I am here, seeking for help...

现在我在这里,寻求帮助……

I know that my code isn't the prettiest and I broke at least two times the idea behind the mvvm pattern. Also I have little understanding of the Invoke method, but I tried my best and searched for a while (2-3 hours) on * and other sites, without succeeding.

我知道我的代码不是最漂亮的,我至少打破了两倍于mvvm模式背后的想法。我也不太了解Invoke方法,但是我尽了最大的努力在*和其他站点上搜索了一段时间(2-3小时),但是没有成功。

So if you bare with me and might be able to help me, I would really appreciate your time and help.

所以如果你能对我坦诚相待,并能帮助我,我将非常感谢你的时间和帮助。

Thanks for anything up ahead.

谢谢你为我做的一切。

greets 10rotator01

问候10 rotator01

EDIT:

编辑:

to specify where the error with thread occurs:

指定出现线程错误的位置:

_mainwindowviewmodel.switchToQuestionView(_isSupportUser);

which leads to the following method

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

This is also one occasion, where I am not using Data Binding. I change the content of my contentcontrol:

这也是一个我不使用数据绑定的场合。我改变了我的内容控件的内容:

 <ContentControl Name="ContentHolder"/>

How would I implement this with Data Binding. Should the property have the type ContentControl? I couldn't really find an answer to this. And by changing this to DataBinding, would the error with the thread ownage be solved?

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

EDIT 2: The project structure is as following: Main View is entry point, in the constructor the data context is set to the mainviewmodel, which is created at that time. the main view has a contentcontrol, where I swap between my usercontrols, in this case my views.

编辑2:项目结构如下:主视图是入口点,在构造函数中,数据上下文被设置为mainviewmodel,该模型是在那个时候创建的。主视图有一个contentcontrol,在用户控件之间进行切换,在本例中是视图。

from my mainviewmodel I set the content of the contentcontrol in the beginning at the usercontrol login, which creates a viewmodel in its contructors and sets it as datacontext.

从我的mainviewmodel开始,我在usercontrol登录处设置内容控件的内容,它在其构造函数中创建一个viewmodel,并将其设置为datacontext。

The code snippets are from my loginviewmodel. Hope this helps.

代码片段来自我的loginviewmodel。希望这个有帮助。

EDIT 3: I thougt I found a workaround, but it still does not work. I forgot, how the timer works in the background, so it can be solved that way either.

编辑3:我以为我找到了一个变通的办法,但它仍然不起作用。我忘了,计时器是如何在后台工作的,所以也可以用这种方法来解决。

2 个解决方案

#1


0  

The problem is that WPF, or XAML framawork in general, doesn't allow to modify visual elements on the main thread, from other threads. For solving this you should to distinguish which is the part of your code that update the view from the second thread. In your case I can see that:

问题是,WPF或XAML framawork通常不允许从其他线程修改主线程上的可视元素。要解决这个问题,您应该区分哪些是代码中更新视图的部分,哪些是第二个线程。就你而言,我可以看出:

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

changes the view. For solving this you could try this answer. In which I use the synchronization context to the communication between threads.

改变了观点。为了解决这个问题,你可以试试这个答案。在其中,我使用同步上下文来处理线程之间的通信。

Another way to solve it, (and it maybe is a wrong usage of the dispatcher) is using the dispatcher for "send" the actions that modify the view to the main thread. Some thing like this:

解决这个问题的另一种方法(可能是对分派器的错误使用)是使用分派器将修改视图的操作“发送”到主线程。一些事情是这样的:

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

Hope this helps...

希望这有助于……

#2


0  

One common approach is to implement an AsyncRelayCommand (in some tutorials also named AsyncDelegateCommand and bind it to the WPF view.

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

Here's an example implementation I used for a demo project to get familiar with WPF, MVVM and DataBinding.

下面是一个示例实现,我在演示项目中使用它来熟悉WPF、MVVM和数据库。

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

Here's the LoginViewModel.

这是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);
            }                
        } );
    }
}

Now you bind Username and Password properties as Two-Way bindings to your text fields and Bind your LoginCommand command to your login button.

现在,您将用户名和密码属性绑定为文本字段的双向绑定,并将LoginCommand命令绑定到登录按钮。

Last but not least, a very basic implementation of the ViewModelBase.

最后但并非最不重要的是,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));
        }
    }
}

Some remarks at the end: There are several issues with your code above, as you already mentioned. You reference the View from ViewModel. This pretty much breaks the whole thing and if you begin to reference views from ViewModel, you can skip MVVM wholly and use WPF's CodeBehind.

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

Also you should avoid referencing other ViewModels form your ViewModel, as this tightly couples them and makes unit-tests pretty hard.

同样,您应该避免引用来自您的ViewModel的其他ViewModel,因为这将它们紧密地结合在一起,并且使单元测试变得非常困难。

To navigate between Views/ViewModels, one usually implement a NavigationService. You define the Interface of the NavigationService (i.e. INavigationService) in your model. But the implementation of the NavigationService happens in the Presentation Layer (i.e. the place/Project where your Views reside), since this is the only place where you can implement a NavigationService.

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

A navigation service is very specific to an application/platform and hence needs to be implemented for each platform a new (Desktop, WinRT, Silverlight). Same goes for the DialogService which displays Dialog messages/popups.

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

#1


0  

The problem is that WPF, or XAML framawork in general, doesn't allow to modify visual elements on the main thread, from other threads. For solving this you should to distinguish which is the part of your code that update the view from the second thread. In your case I can see that:

问题是,WPF或XAML framawork通常不允许从其他线程修改主线程上的可视元素。要解决这个问题,您应该区分哪些是代码中更新视图的部分,哪些是第二个线程。就你而言,我可以看出:

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

changes the view. For solving this you could try this answer. In which I use the synchronization context to the communication between threads.

改变了观点。为了解决这个问题,你可以试试这个答案。在其中,我使用同步上下文来处理线程之间的通信。

Another way to solve it, (and it maybe is a wrong usage of the dispatcher) is using the dispatcher for "send" the actions that modify the view to the main thread. Some thing like this:

解决这个问题的另一种方法(可能是对分派器的错误使用)是使用分派器将修改视图的操作“发送”到主线程。一些事情是这样的:

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

Hope this helps...

希望这有助于……

#2


0  

One common approach is to implement an AsyncRelayCommand (in some tutorials also named AsyncDelegateCommand and bind it to the WPF view.

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

Here's an example implementation I used for a demo project to get familiar with WPF, MVVM and DataBinding.

下面是一个示例实现,我在演示项目中使用它来熟悉WPF、MVVM和数据库。

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

Here's the LoginViewModel.

这是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);
            }                
        } );
    }
}

Now you bind Username and Password properties as Two-Way bindings to your text fields and Bind your LoginCommand command to your login button.

现在,您将用户名和密码属性绑定为文本字段的双向绑定,并将LoginCommand命令绑定到登录按钮。

Last but not least, a very basic implementation of the ViewModelBase.

最后但并非最不重要的是,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));
        }
    }
}

Some remarks at the end: There are several issues with your code above, as you already mentioned. You reference the View from ViewModel. This pretty much breaks the whole thing and if you begin to reference views from ViewModel, you can skip MVVM wholly and use WPF's CodeBehind.

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

Also you should avoid referencing other ViewModels form your ViewModel, as this tightly couples them and makes unit-tests pretty hard.

同样,您应该避免引用来自您的ViewModel的其他ViewModel,因为这将它们紧密地结合在一起,并且使单元测试变得非常困难。

To navigate between Views/ViewModels, one usually implement a NavigationService. You define the Interface of the NavigationService (i.e. INavigationService) in your model. But the implementation of the NavigationService happens in the Presentation Layer (i.e. the place/Project where your Views reside), since this is the only place where you can implement a NavigationService.

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

A navigation service is very specific to an application/platform and hence needs to be implemented for each platform a new (Desktop, WinRT, Silverlight). Same goes for the DialogService which displays Dialog messages/popups.

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