0

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;
    }
}
Cœur
  • 37,241
  • 25
  • 195
  • 267
John Miner
  • 893
  • 1
  • 15
  • 32
  • 1
    are you sure you put the correct thread's state to STA? also, http://stackoverflow.com/questions/2329978/the-calling-thread-must-be-sta-because-many-ui-components-require-this and many others ask the exact same – stijn Jul 23 '12 at 10:21
  • 1
    I'd just use the `Dispatcher` to send the LoadModules command to the main UI thread :) – Rachel Jul 23 '12 at 13:14
  • @Rachel - but isn't using 'Dispatcher' bad for testing purposes ? and also - I thought the 'View-Model' should know nothing about the UI, and if I use the 'Dispatcher' to do something on the UI thread - that beats the purpose a bit, no ? – John Miner Jul 24 '12 at 05:40
  • @stijn - I am sure. I created a new thread just like in the link you have provided, and I set it's apartment state to STA. and inside that thread's function - I try to load the modules and then I get that exception – John Miner Jul 24 '12 at 05:41
  • 1
    can you post the simplest example that fails? I'd like to test this, but my apps don't use LoadModule like that so I need code.. Also, nothing wrong with Dispatcher for testsing: just hide it behind an interface eg http://msdn.microsoft.com/en-us/library/microsoft.practices.composite.presentation.events.idispatcherfacade.aspx – stijn Jul 24 '12 at 07:21
  • @stijn - I have updated my question to include code that should clarify my problem... – John Miner Jul 24 '12 at 08:28
  • @stijn - maybe the problem is this : even though I spawn a separate thread and set it to 'STA', in this thread I call an ASYNC operation on a WCF client - so MAYBE when the call returns ("end invoke") and I try to do 'LoadModule' - this no longer is on the spawned 'STA' thread ? maybe that is the problem ? – John Miner Jul 24 '12 at 08:30
  • 1
    lol you beat me to it,, that's just what I started to type out in detail :] – stijn Jul 24 '12 at 08:41

1 Answers1

2

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.

stijn
  • 34,664
  • 13
  • 111
  • 163
  • Excellent way to put it. Dispatcher works. Just to clarify - I do need a separate thread for 'LoginWork' because connecting to the WCF service might take 1-2 seconds, and I don't want that to hang the UI thread, so that's why I put 'LoginWork' in a different thread. – John Miner Jul 24 '12 at 11:31
  • That being said - it seems stupid now to use the 'ASYNC' mode of calling, since it is not done on the UI thread anyway. So I might as well just call the SYNC operation and when it finished marshal the 'LoadModule' via the dispatcher... – John Miner Jul 24 '12 at 11:32
  • in that case, yes, that's the best option. – stijn Jul 24 '12 at 12:56