virtual.olympus.blog musings from the end of a lightning bolt

Silverlight 4 Out-of-Browser–Windows Authentication Timeout

Tags: EF4, Silverlight, WCF RIA Services

Introduction

This will be a pretty short and sweet blog post, but the issue that I describe caused me enough headaches, and was hard enough to ‘Google’ that adding more ways to get there is in and of itself worth it.  The problem I ran into was with a Silverlight 4 RIA Services application which utilizes Windows Authentication and needs to run as an out of browser application.  The application needed to get detailed user information in order to properly construct the initial application user interface.  The problem is that the Authentication Service call was timing out whenever I ran the application, and my RIA Service was never even executed.  I’ve tried to trim down the example to the bare minimum, but I decided to stop trimming at the point where I actually removed the RIA service entirely Smile  So, although the issue does not require RIA Services to reproduce, it will often be encountered when implementing a RIA Service application.

 

The Problem

The timeout problem we were seeing was only occurring when the application was running out of the browser.  The problem was complicated by the fact that the NetworkInterface.GetIsNetworkAvailable() routine was returning ‘true’, indicating that a network connection was available, but all of the network access that we tested would result in a timeout.  There did not appear to be anything causing this timeout, and I was at an impasse until I stumbled upon this forum post which mentioned something about the RootVisual of the application.  I’m not certain how they discovered the RootVisual link, but that is the key.

 

Example Code

The following code snippet outlines an example of the original code:

    public partial class App : Application
    {

        public App()
        {
            this.Startup += this.Application_Startup;
            this.Exit += this.Application_Exit;
            this.UnhandledException += this.Application_UnhandledException;

            WebContext context = new WebContext();
            context.Authentication = new WindowsAuthentication();
            this.ApplicationLifetimeObjects.Add(context);

            InitializeComponent();
        }

        private void Application_Startup(object sender, StartupEventArgs e)
        {
            this.Resources.Add("WebContext", WebContext.Current);
            this.Resources.Add("Role-Administrator", RoleConstants.Administrator);
            this.Resources.Add("Role-Flunky", RoleConstants.Flunky);

            if (!NetworkInterface.GetIsNetworkAvailable())
            {
                MessageBox.Show("Here I am before RootVisual is set and the network isn't available.");
                return;
            }
            else
            {
                MessageBox.Show("Currently showing as network access is enabled...");
            }

            // if we're running out of browser, then use the ClientHttp stack...
            if (App.Current.IsRunningOutOfBrowser)
            {
                HttpWebRequest.RegisterPrefix("http://", WebRequestCreator.ClientHttp);
                HttpWebRequest.RegisterPrefix("https://", WebRequestCreator.ClientHttp);

                App.Current.CheckAndDownloadUpdateCompleted += (s, args) =>
                {
                    if (args.UpdateAvailable)
                    {
                        MessageBox.Show("An update has been downloaded. " +
                            "Restart the application to run the new version.");
                    }
                    else if (args.Error != null &&
                             args.Error is PlatformNotSupportedException)
                    {
                        MessageBox.Show("An application update is available, " +
                            "but it requires a new version of Silverlight. " +
                            "Visit the application home page to upgrade.");
                    }
                };
                App.Current.CheckAndDownloadUpdateAsync();
            }

            WebContext.Current.Authentication.LoadUser(
                (loadOp) =>
                {
                    if (!loadOp.HasError)
                    {
                        AppContext.CurrentUser = loadOp.User as CustomPrincipal;

                        // setup the 'default' context
                        AppContext.ApplicationUser = new UserContext()
                        {
                            UserId = AppContext.CurrentUser.CustomIdentity.UserID,
                            HasReadAccess = true,
                            HasWriteAccess = AppContext.CurrentUser.IsInRole(RoleConstants.Administrator)
                        };

                        ///***NOTE: This bit works fine in browser, but never gets executed OOB
                        AppContext.RootView = new MainPage();
                        this.RootVisual = AppContext.RootView;

                        // now load the official context from the server
                        SilverlightRIAServicesOOBDomainContext context = new SilverlightRIAServicesOOBDomainContext();
                        context.Load(
                            context.GetUserByIdQuery(AppContext.ApplicationUser.UserId),
                            LoadBehavior.MergeIntoCurrent,
                            (userOp) =>
                            {
                                if (!userOp.HasError)
                                {
                                    AppContext.ApplicationUser.Identity = userOp.Entities.FirstOrDefault();
                                    AppContext.ApplicationUser.Role = AppContext.ApplicationUser.Identity.Role;
                                }
                                else
                                {
                                    MessageBox.Show(ErrorMessages.ExceptionMessage(userOp.Error));
                                }
                            },
                            null);

                    }
                    else
                    {
                        MessageBox.Show(ErrorMessages.ExceptionMessage(loadOp.Error));
                    }
                },
                null);

        }
    }

 

As you can see above, this is fairly standard code.  The App constructor handles creating the WebContext instance and assigning the WindowsAuthentication class to automatically use the current logged in user to authenticate with the RIA Service.  Then, when the application starts up, the AuthenticationService::LoadUser method is called to pull down detailed user principal/identity information to do things like indicate who is currently logged in, control access to portions of the UI based on user role(s), etc.  In this case, after the LoadUser call, there’s a RIA Service call to get detailed information about the user that isn’t included in the IPrincipal/IIdentity implementation.

When you run this code inside the browser, everything functions as expected.  However, once you install the application out of browser, the initial LoadUser service call times out and the application remains at the ‘white screen’ due to the RootVisual not being set to our primary application view.

 

The Solution

The solution, as mentioned in the forum post linked above, is to initialize the Root Visual to SOMETHING prior to any service calls or network access.  In this case, I didn’t want my view to be initialized immediately, because it required details about the current user [e.g. where to navigate, what controls should appear, etc] and there really wasn’t any purpose served by showing a ‘loading’ screen or some such.  So, I simply assigned a ‘Grid’ to the RootVisual, and then assigned my actual RootView as the first [and only] child of the grid.  The fixed code looks like this:

    public partial class App : Application
    {

        public App()
        {
            this.Startup += this.Application_Startup;
            this.Exit += this.Application_Exit;
            this.UnhandledException += this.Application_UnhandledException;

            WebContext context = new WebContext();
            context.Authentication = new WindowsAuthentication();
            this.ApplicationLifetimeObjects.Add(context);

            InitializeComponent();
        }

        private void Application_Startup(object sender, StartupEventArgs e)
        {
            this.Resources.Add("WebContext", WebContext.Current);
            this.Resources.Add("Role-Administrator", RoleConstants.Administrator);
            this.Resources.Add("Role-Flunky", RoleConstants.Flunky);

            if (!NetworkInterface.GetIsNetworkAvailable())
            {
                MessageBox.Show("Here I am before RootVisual is set and the network isn't available.");
                return;
            }
            else
            {
                MessageBox.Show("Currently showing as network access is enabled...");
            }

            ///***NOTE:  This bit fixes the OOB problems...
            this.RootVisual = new Grid();

            if (!NetworkInterface.GetIsNetworkAvailable())
            {
                MessageBox.Show("Here I am after RootVisual is set and the network isn't available.");
                return;
            }

            // if we're running out of browser, then use the ClientHttp stack...
            if (App.Current.IsRunningOutOfBrowser)
            {
                HttpWebRequest.RegisterPrefix("http://", WebRequestCreator.ClientHttp);
                HttpWebRequest.RegisterPrefix("https://", WebRequestCreator.ClientHttp);

                App.Current.CheckAndDownloadUpdateCompleted += (s, args) =>
                {
                    if (args.UpdateAvailable)
                    {
                        MessageBox.Show("An update has been downloaded. " +
                            "Restart the application to run the new version.");
                    }
                    else if (args.Error != null &&
                             args.Error is PlatformNotSupportedException)
                    {
                        MessageBox.Show("An application update is available, " +
                            "but it requires a new version of Silverlight. " +
                            "Visit the application home page to upgrade.");
                    }
                };
                App.Current.CheckAndDownloadUpdateAsync();
            }

            WebContext.Current.Authentication.LoadUser(
                (loadOp) =>
                {
                    if (!loadOp.HasError)
                    {
                        AppContext.CurrentUser = loadOp.User as CustomPrincipal;

                        // setup the 'default' context
                        AppContext.ApplicationUser = new UserContext()
                        {
                            UserId = AppContext.CurrentUser.CustomIdentity.UserID,
                            HasReadAccess = true,
                            HasWriteAccess = AppContext.CurrentUser.IsInRole(RoleConstants.Administrator)
                        };

                        ///***NOTE: This bit is now fixed since RootVisual can't be set twice
                        AppContext.RootView = new MainPage();
                        ((Grid)this.RootVisual).Children.Add(AppContext.RootView);

                        // now load the official context from the server
                        SilverlightRIAServicesOOBDomainContext context = new SilverlightRIAServicesOOBDomainContext();
                        context.Load(
                            context.GetUserByIdQuery(AppContext.ApplicationUser.UserId),
                            LoadBehavior.MergeIntoCurrent,
                            (userOp) =>
                            {
                                if (!userOp.HasError)
                                {
                                    AppContext.ApplicationUser.Identity = userOp.Entities.FirstOrDefault();
                                    AppContext.ApplicationUser.Role = AppContext.ApplicationUser.Identity.Role;
                                }
                                else
                                {
                                    MessageBox.Show(ErrorMessages.ExceptionMessage(userOp.Error));
                                }
                            },
                            null);

                    }
                    else
                    {
                        MessageBox.Show(ErrorMessages.ExceptionMessage(loadOp.Error));
                    }
                },
                null);

        }

    }

 

This simple fix enables the application to work properly both in and out of browser, however, there is a secondary side effect if you’re using ChildWindow(s) for dialogs.  The ChildWindow has a nasty bug in that it intermittently decides to not re-enable your underlying controls when the ChildWindow is closed.  The accepted workaround for this is to set the Control.IsEnabled dependency property to ‘true’ on the RootVisual in the Closing event of the ChildWindow.  Unfortunately, when the RootVisual is a Grid, doing so causes an ‘Unexpected Error’ unhandled exception to be thrown by the Silverlight runtime and things get a little weird.  To get around this, we store our ‘actual’ root visual in a static class and then just set the IsEnabled property on that static variable.  This results in the same [correct] behavior as before and the exception is averted.

 

Final Thoughts

It would be nice if this wasn’t an issue at all, and I’m uncertain WHY the RootVisual needs to be set prior to any networking calls, but so long as that remains the case, the NetworkInterface.GetIsNetworkAvailable() method should return ‘false’ until the RootVisual is set properly.  Again, why the network access is disabled is a mystery, and feels like a pretty large bug.  In any case, if you run into this problem like we did, hopefully you can avoid banging your head against the wall as we did trying to find the solution.  The solution we’ve come up with won’t work for everyone, but hopefully it’ll get you started in the right direction.

 

Sample Code:  SilverlightRIAServiceOOB-20110410.zip (226 kB)

4 Comments

  • Greg said

    Thanks, I was attempting to do something similar and could not figure out why the timeout was occurring. It seems kind of strange that no one has run across this before. I would imagine trying to load a user in the app.xaml.cs before displaying the initial screen would be a pretty common scenario.

  • jeff hogan said

    I'm hoping you can help me troubleshoot: I get the below WCF Binary errors in Fiddler System.NullReferenceException: Object reference not set to an instance of an object. at BinaryMessageFiddlerExtension.BinaryInspector.CreateReaderForMessage(Byte[] encodedMessage) at BinaryMessageFiddlerExtension.BinaryInspector.LoadMessageIntoDocument(Byte[] encodedMessage) at BinaryMessageFiddlerExtension.BinaryInspector.GetWcfBinaryMessageAsText(Byte[] encodedMessage) at BinaryMessageFiddlerExtension.BinaryInspector.UpdateView(Byte[] bytes) I also see this in the 'raw' part in Fiddler: GET http://www.virtual-olympus.com/AccuSL/ClientBin/WCFRIA2.xap HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0 (Windows NT 5.2; rv:9.0.1) Gecko/20100101 Firefox/9.0.1 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip, deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Connection: keep-alive What I'm wondering (and I'm a bit of a novice at this) is if I should see something in the "Accept" line that says it accepts .xap files. I get the same errors if I choose the actual URL (and not local host) GET http://xyz.xyz/AccuSL/ClientBin/WCFRIA2.xap HTTP/1.1 Accept: */* Accept-Encoding: gzip, deflate User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET4.0C; .NET4.0E) Host: xyz.xyz Connection: Keep-Alive Which I see a wildcard in the 'accept' so am unsure if the fact that I don't see .xap in the localhost error means anthing. The WCF binary error is the same via localohost and URL

  • TheJet said

    Jeff, Why don't you hit me up over twitter [@virtualolympus] or email [ben@virtual-olympus.com], Fiddler is going to have an issue doing any sort of WCF binary deserialization when downloading the XAP file to launch the Silverlight app, so I'm thinking this is the wrong error message or you're not troubleshooting the problem you think you are :) Thanks for reading!

  • bob said

    Hey thank you so much for this article, it's very odd but setting RootVisual fixed my issue on silverlight 5 out of browser app. Before setting the RootVisual all the network calls would just hang until the timeout hit. Thanks again.

Comments have been disabled for this content.