简体   繁体   中英

C# WPF MVVM Blocking UI Thread

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.

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

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)

I have a display in the view, which is like a progress bar. It is active when PanelLoading equals 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).

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

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

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 stackoverflow and other sites, without succeeding.

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?

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.

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.

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

I thought 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.

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:

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

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

Here's an example implementation I used for a demo project to get familiar with WPF, MVVM and 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();
    }
}

Here's the 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.

Last but not least, a very basic implementation of the 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.

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

To navigate between Views/ViewModels, one usually implement a NavigationService. You define the Interface of the NavigationService (ie INavigationService ) in your model. But the implementation of the NavigationService happens in the Presentation Layer (ie the place/Project where your Views reside), since this is the only place where you can implement a 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM