C# WPF MVVM 阻塞 UI 线程

3

我不是很确定我的问题/错误出在哪里。 我正在使用WPF和MVVM模式结合,在登录时遇到了问题。

我的第一次尝试很好用。我有几个窗口,每个窗口都有自己的ViewModel。 在登录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,首先检查登录是否成功,然后检查用户是否具有特定的AD组(在isSupport方法中)。

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

到目前为止一切正常。

然后我创建了一个带有contentcontrol的主窗口,并将我的视图更改为用户控件,以便可以交换它们。(意图是不为每个视图打开/创建一个新窗口)。

现在执行代码时,我的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)

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

    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.

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

我知道我的代码不是最漂亮的,也至少两次违反了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类型吗?我没有真正找到答案。将其更改为数据绑定,能解决线程所有权的错误吗?
项目结构如下: Main View是入口点,在构造函数中设置数据上下文为mainviewmodel,此时创建了它。主视图有一个contentcontrol,在其中交换用户控件,例如我的视图。
从我的mainviewmodel中,我在开始时设置contentcontrol的内容为usercontrol login,它在其构造函数中创建一个viewmodel,并将其设置为数据上下文。
这些代码片段来自于我的loginviewmodel。希望这可以帮助你。
我认为我已经找到了一种解决方法,但它仍然无法工作。我忘记了计时器在后台的工作方式,所以也可以通过这种方式解决。

请参考我在Stack Overflow上的答案,关于在WPF和MVVM中将属性更改通知调度到UI线程的封送。这可能会对您有所帮助。My Answer - Federico Berasategui
你正在使用哪个.NET框架版本? - Yuval Itzchakov
你应该发布更完整的代码。你发布了一些没有任何上下文的代码。那个代码在哪里??它在控制器中吗?如果是这样,那么UI在AD查询期间会被锁定也就不足为奇了,因为只有在创建ViewModel之后才能以任何方式将其绑定到UI。 - Tseng
1
旁注--放弃创建线程,加入任务车。 - user1228
你的“方法”是通过ICommand接口绑定在WPF中的吗?如果是,那么有一些基于Async ICommands的示例基类。我回家后会查找链接。 - Tseng
显示剩余5条评论
2个回答

0
问题是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();

希望这能有所帮助...

0

一个常见的方法是实现一个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引用View,这几乎破坏了整个东西,如果您开始从ViewModel引用视图,则可以完全跳过MVVM并使用WPF的CodeBehind。
此外,您应该避免从ViewModel引用其他ViewModel,因为这会使它们紧密耦合并使单元测试非常困难。
要在视图/ViewModel之间导航,通常需要实现NavigationService。您在模型中定义NavigationService的接口(即INavigationService)。但是,NavigationService的实现发生在Presentation Layer中(即您的Views所在的地方/项目),因为这是您可以实现NavigationService的唯一位置。
导航服务非常特定于应用程序/平台,因此需要为每个新平台(桌面,WinRT,Silverlight)实现它。同样适用于显示对话框消息/弹出窗口的DialogService。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接