At the beginning of the year, as part of a series of posts about the INavigationContentLoader extensibility point in Silverlight 4, I described a way to use a content loader to do authorization before allowing a user to navigate to a page.  With the content loader, you can either throw an exception when an unauthorized user tries to reach a protected Page, redirect your users to another Page, or return a different page (e.g. a Login page) in its stead.  This makes for a fairly nice experience for your users, wherein they are taken directly to a login page (or at least a page with more information about why they cannot access the given page) when they lack the credentials to reach the page they are requesting.

The trouble with this, however, was that once your application reached the login page and your user attempted to log in, there was no clear/easy/universal way to get the user back to the location he/she was originally requesting.  Ideally, an application would keep its context (i.e. the Uri wouldn’t change) when it sends a user to a login page, and take the user to the restricted content once the right credentials are acquired.

When I wrote my original post, I was aware of this limitation, and didn’t have a great solution for it.  Attempting to re-navigate to the requested page was unhelpful because navigating twice to the same Uri is a no-op.  Starting with the Silverlight 4 RC (and continuing into the RTW release, of course), however, such a solution exists!  We quietly added an API to Frame and NavigationService: Refresh().

How does refreshing help?

Calling Frame.Refresh() or NavigationService.Refresh() causes the entire page to be reloaded, meaning that a custom content loader will be called, providing an opportunity to return a different page (or redirect elsewhere).  Without having to make any changes to SLaB and the AuthContentLoader or ErrorPageLoader, we can now produce the desired experience!

Now, our ContentLoader XAML looks like this:

<navigation:Frame x:Name="ContentFrame"
                    Style="{StaticResource ContentFrameStyle}"
                    Source="/Home">
    <navigation:Frame.UriMapper>
        <uriMapper:UriMapper>
            <uriMapper:UriMapping Uri=""
                                    MappedUri="/Views/Home.xaml" />
            <uriMapper:UriMapping Uri="/{pageName}"
                                    MappedUri="/Views/{pageName}.xaml" />
        </uriMapper:UriMapper>
    </navigation:Frame.UriMapper>
    <navigation:Frame.ContentLoader>
        <SLaB:ErrorPageLoader>
            <SLaB:ErrorPage ExceptionType="UnauthorizedAccessException"
                            ErrorPageUri="/Views/LoginPage.xaml" />
            <SLaB:ErrorPage ErrorPageUri="/Views/ErrorPage.xaml" />
            <SLaB:ErrorPageLoader.ContentLoader>
                <SLaB:AuthContentLoader Principal="{Binding User, Source={StaticResource WebContext}}">
                    <SLaB:NavigationAuthorizer>
                        <SLaB:NavigationAuthRule UriPattern="^/Views/About\.xaml\??.*$">
                            <SLaB:Deny Users="?" />
                            <SLaB:Allow Users="*" />
                        </SLaB:NavigationAuthRule>
                        <SLaB:NavigationAuthRule UriPattern="^/Views/RegisteredUsersPage\.xaml\??.*$">
                            <SLaB:Allow Roles="Registered Users" />
                        </SLaB:NavigationAuthRule>
                    </SLaB:NavigationAuthorizer>
                </SLaB:AuthContentLoader>
            </SLaB:ErrorPageLoader.ContentLoader>
        </SLaB:ErrorPageLoader>
    </navigation:Frame.ContentLoader>
</navigation:Frame>

The primary difference between the XAML above and the original XAML I had posted was to remove the ErrorRedirector (which caused redirection to the login page rather than loading the login page in place of the requested page).  Because this was removed, we no longer need nested ErrorPageLoaders (which existed in order to redirect only in the login case, and load the error page without changing the Uri for other errors).  You’ll note that for the About page and the RegisteredUsers page, access is restricted.  When an UnauthorizedAccessException occurs, users will see the LoginPage.

In the login page, all we need to do now is call NavigationService.Refresh() when the user logs in.  My example uses WCF RIA Service’s WebContext find out this information, but you could just as easily attempt to refresh after a ChildWindow is closed or a Login button is clicked.

My LoginPage code looks like this:

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    WebContext.Current.Authentication.LoggedIn += Authentication_LoggedIn;
}

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
    WebContext.Current.Authentication.LoggedIn -= Authentication_LoggedIn;
}

void Authentication_LoggedIn(object sender, AuthenticationEventArgs e)
{
    NavigationService.Refresh();
}

Yep, that’s all it takes!  Now, when a user logs in (either by clicking the login button on the page or logging in through some other dialog in the application), the Frame’s content is refreshed, and the AuthContentLoader attempts to verify the user’s credentials once again.

Cool!  Can I see it in action?

You know I would never leave you without a sample!  Click the image below to see the sample application (based on my original example, just updated for SL4).  First try navigating to the protected pages without logging in, then try logging in and note how the page automatically is refreshed based upon your new credentials.

Login information: Log in with User = “Test”, Password = “_Testing”

A WCF RIA Services application with the AuthContentLoader

 

You can find the source for this application here.

Anything else I should know about Refresh()?

Without a doubt, Refresh()’s usefulness is not restricted to this scenario.  With custom content loaders, it’s particularly useful to be able to refresh the page, since the page returned as a result of that navigation may change from one attempt to the next.  Even without a custom content loader, Refresh() allows you to create a new instance of a page, making re-initializing the page you’ve navigated to clean and simple.  The behavior is identical to navigating to a new page – the only difference is that the old and new Uris are identical, and the NavigationMode of the operation is “Refresh”.

Please note: Refresh() will still respect the NavigationCacheMode of the Page and the CacheSize of the Frame.  If a Page is being cached, calling Refresh() will not create a new instance (but will still cause the Navigating/Navigated events and the corresponding overrides on Page to be raised/called).  To prevent this from happening, set the NavigationCacheMode of the page being refreshed to Disabled before the new page would be loaded (i.e. before Refresh() is called or while handling the Navigating event).

Is that it?

Yep, that’s it! :)  Let me know what you think!  What else would you like to see?