簡體   English   中英

C#WPF MVVM阻止UI線程

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM