C#WPF MVVM阻止UI线程

问题描述 投票:2回答:2

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

我的第一次尝试工作正常。我有几个窗口,每个窗口都有自己的ViewModel。在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";

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

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

到现在为止一切正常。

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

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

  • 将代码段移动到另一个方法并将其作为自己的线程启动: Thread t1 = new Thread(() => loginThread()); t1.SetApartmentState(ApartmentState.STA); t1.Start(); 当我这样做时,我得到一个错误,即资源由另一个线程拥有,因此无法访问。 (调用线程无法访问此对象,因为另一个线程拥有它)
  • 然后,尝试调用登录部分而不是其他线程;包含先前代码段的登录 Application.Current.Dispatcher.Invoke((Action)(() => { login(); })); 这不起作用。至少不是我如何实现它。
  • 之后,我尝试仅在一个线程中运行登录片段的主要部分,然后完成后,引发先前注册的事件,该事件将处理内容控件的更改。这是部分,我得到错误的线程访问另一个线程拥有的资源,所以我想,我可以解决这个问题。 void HandleThreadDone(object sender, EventArgs e) { if (_isValid) { PanelLoading = false; _mainwindowviewmodel.switchToQuestionView(_isSupportUser); } else PanelMainMessage = "Verbindung zum Server konnte nicht hergestellt werden"; } 在login方法中我会调用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,是否可以解决线程所有权的错误?

项目结构如下:Main View是入口点,在构造函数中,数据上下文设置为mainviewmodel,该时间是在此时创建的。主视图有一个contentcontrol,我在我的usercontrols之间交换,在这种情况下我的视图。

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

代码片段来自我的loginviewmodel。希望这可以帮助。

我以为我找到了一个解决方法,但它仍然无效。我忘了,计时器如何在后台运行,所以它也可以这样解决。

c# wpf multithreading mvvm blocking
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,因为这会紧密地耦合它们并使单元测试非常困难。

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

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

© www.soinside.com 2019 - 2024. All rights reserved.