Question

I have a WPF application in PRISM architecture.

I have a 'Login View' that is shown in the 'Main Region' when the app loads.

When the user presses 'Login' - I connect to a WCF service, authenticate the user, and get a list of roles for that user from the service.

Then - according to the user's roles - I load different modules, using the 'Module Manager'.

Problem is - I want all the work after the 'Login' button is pressed to be done in a separate thread, because it might take time to connect to the service etc, and I don't want the UI to be frozen.

But - if I put the code to 'connect, authenticate, get roles, load modules' in a separate thread - I get an exception when I call '_moduleManager.LoadModule' that says:

The calling thread must be STA, because many UI components require this.

How can I solve this ?

I have tried different solutions. I have tried to set the new thread's 'Apartment State = STA' and it didn't help. I thought about saving the 'Dispatcher' object in the constructor of the View-Model, and then do 'dispatcher.Invoke' when I call 'LoadModule', but that is bad design (View-Model should not use Dispatcher, and also it is bad for testing).

Any ideas how I can solve this ??

Only the 'LoadModule' gives me grief, all the other stuff works fine.

.

[Update] - Added Code Sample :

[Export]
public class LoginViewModel : NotificationObject
{
    [ImportingConstructor]
    public LoginViewModel(IRegionManager regionManager, IModuleManager moduleManager)
    {
        this.LoginCommand   = new DelegateCommand(LoginExecute, LoginCanExecute);
        this._regionManager = regionManager;
        this._moduleManager = moduleManager;
    }

    private void LoginExecute()
    {
        IsBusy = true; // Set this to 'true' so controls go disabled
        LoginStatus = ""; // Clear the 'login status' string


        Thread loginThread = new Thread(new ThreadStart(LoginWork));
        loginThread.SetApartmentState(ApartmentState.STA);
        loginThread.Start();
    }

    private void LoginWork()
    {
        ParamsToGetRoles param = new ParamsToGetRoles
        {
            Username = Username,
            InputtedPassword = Password
        };

        try
        {
            // Connect to the secure service, and request the user's roles
            _clientSecure = new AuthenticationServiceClient("WSHttpBinding_MyService");
            _clientSecure.ClientCredentials.UserName.UserName = param.Username;
            _clientSecure.ClientCredentials.UserName.Password = param.InputtedPassword;
            _clientSecure.ChannelFactory.Faulted += new EventHandler(ChannelFactory_Faulted);
            var local = _clientSecure.ChannelFactory.CreateChannel();
            _clientSecure.GetRolesCompleted += new EventHandler<GetRolesCompletedEventArgs>(clientSecure_GetRolesCompleted);
            _clientSecure.GetRolesAsync(param);
        }
        catch (Exception ex)
        {
        Console.WriteLine("Exception : " + ex.Message.ToString());
        }
    }

    void clientSecure_GetRolesCompleted(object sender, GetRolesCompletedEventArgs e)
    {
        if (e.Error == null)
        {
            _clientSecure.Close();
            LoginSuccess(e.Result.UserRoles);
        }
        else
        {
            LoginFailure("Unable to authenticate");
        }
        _clientSecure = null;
    }

    private void LoginSuccess(List<UserTypeEnum> rolesOfAuthenticatedUser)
    {
        LoginStatus = "Success";

        if (rolesOfAuthenticatedUser.Contains(UserTypeEnum.Administrator))
        {
            // This is what throws the exception !
            // This is called by the 'EndInvoke' of the 'GetRoles' operation, 
            // Which was called in the 'LoginWork' function which was run on a separate thread !
            _moduleManager.LoadModule(WellKnownModuleNames.ModuleAdmin);
        }

        NavigateToMainMenu();

        this.IsBusy = false;
    }
}
Was it helpful?

Solution

You should attach the debugger and inspect the threads window with a breakpoint set at clientSecure_GetRolesCompleted. I'm pretty sure it is not being called from the loginThread: while LoginWork does run in the loginThread, it then adds an eventhandler to the completion event of an async operation. Async = runs in yet another thread.

So what probably happens:

  • LoginExecute executes in the UI thread
  • starts a seperate thread B to run LoginWork
  • calls GetRolesAsync so start a thread C (which is not STA) to get the roles
  • thread C eventually calls 'clientSecure_GetRolesCompleted', not thread B

So, you do not need a seperate thread for LoginWork since the actual work is already done as an async operation. To get around the loading issue, either try to make the 'get roles' thread STA, or better, use a dispatcher so LoginSuccess gets invoked on the UI thread.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top