[英]C# WPF MVVM Blocking UI Thread
我不太確定我的問題/錯誤在哪里 。 我將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,首先檢查登錄是否成功,然后檢查用戶是否具有某個廣告組(在isSupport方法中)
我在視圖中有一個顯示,就像一個進度條。 當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"; }
在登錄方法中,我將調用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 Name="ContentHolder"/>
我將如何使用數據綁定來實現這一點。 該屬性應具有ContentControl類型嗎? 我真的找不到答案。 通過將其更改為DataBinding,是否可以解決線程所有權問題?
項目結構如下:Main View是入口點,在構造函數中,數據上下文設置為那時創建的mainviewmodel。 主視圖有一個contentcontrol,在我的用戶控件之間進行交換,在本例中為我的視圖。
從我的mainviewmodel中,我在usercontrol登錄名的開頭設置了contentcontrol的內容,這會在其構造函數中創建一個viewmodel並將其設置為datacontext。
代碼段來自我的loginviewmodel。 希望這可以幫助。
我以為我找到了一種解決方法,但是仍然無法解決。 我忘記了計時器在后台的工作方式,因此也可以通過這種方式解決。
問題在於,WPF或XAML架構通常不允許從其他線程修改主線程上的可視元素。 為了解決這個問題,您應該區分代碼的哪一部分是從第二個線程更新視圖的。 就您而言,我可以看到:
_view.ContentHolder.Content = new SwitchPanel(supportUser);
更改視圖。 為了解決這個問題,您可以嘗試這個答案 。 我在其中使用同步上下文進行線程之間的通信。
解決該問題的另一種方法(可能是對調度程序的錯誤使用)是使用調度程序將修改視圖的操作“發送”到主線程。 像這樣的東西:
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();
希望這可以幫助...
一種常見的方法是實現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);
}
} );
}
}
現在,您將“ Username
和“ Password
屬性作為雙向綁定綁定到文本字段,並將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的實現發生在Presentation層(即視圖所在的位置/項目)中,因為這是可以實現NavigationService的唯一位置。
導航服務是特定於應用程序/平台的,因此需要為每個平台實現新的服務(桌面,WinRT,Silverlight)。 顯示對話框消息/彈出窗口的DialogService也是如此。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.