Displaying a login dialog based on page attributes in Silverlight 3

by StefanOlson 19. June 2009 15:44

Update: The latest version of the code is available for download here. This page has been re-written for the .net ria Services July CTP.

One of the feature requests I described in my post Issues with the Silverlight Navigation architecture was the ability to use an attribute to declare the required roles to display a particular page.  e.g:

[RequiresAuthentication]
public partial class CategoryPage : Page

Unfortunately this is not part of Silverlight 3, so I set about implementing it myself based on the custom URI mapper I have developed in previous posts.

Let me introduce the AuthenticatingUriMapper!  You can download the source code and example here.

All source code described below is part of the sample.  It shows how you can provide attributes on a page to automatically have a login dialog displayed when navigating to it if required!  The sample is based on the Silverlight business application template, that is installed as part of the .net RIA services.  Sample requires the .net RIA services to be installed.

e.g:

image

Using it couldn't be simpler! Just like the custom URI mapper, you add it as your uriMapper (in the Frame or as a StaticResource):

<Navigation2:AuthenticatingUriMapper DefaultPage="/Views/EmptyPage.xaml">
    <Navigation2:CustomUriMapper.CustomUriMappings>
        <UriMappingNavigationSample:AboutBoxRoute/>
        <routing:CustomRoute/>
    </Navigation2:CustomUriMapper.CustomUriMappings>
</Navigation2:AuthenticatingUriMapper>

See my previous post for details on the default page attribute used above.

Then in your mainpage constructor, you tell it about the frame and handle any authentication requests:

public MainPage()
{
    InitializeComponent();
    this.loginContainer.Child = new LoginControl();
    
    (ContentFrame.UriMapper as AuthenticatingUriMapper).Frame = frame;
    (ContentFrame.UriMapper as AuthenticatingUriMapper).AuthenticationRequired += MainPage_AuthenticationRequired;
}

In the MainPage_AuthenticationRequired function, you simply display the login dialog.  We are passing the event arguments so that later on, when login has been completed, we can verify , that , the new login credentials are valid for the page the user wishes to go to.

void MainPage_AuthenticationRequired(object sender, AuthenticationRequiredEventArgs e)
{
    LoginWindow dialog = new LoginWindow { AuthenticationRequiredEventArgs = e};
    dialog.Show();
}

Then, in the pages we wish to require authentication for, add either of the following two attributes:

[RequiresAuthentication]
public partial class UserPage : Page

Or, if you need specific roles:

[RequiresRoles("Administrator")]
public partial class AdminPage : Page

In the login dialog we can make some changes to make use of the AuthenticationRequiredEventArgs.

When the login is successful , we need to verify that now that the user is logged in they passed the authentication requirements of the page they are going to . Here's how we do this:

else if (loginOp.LoginSuccess)
{
    DialogResult = true;
    if (AuthenticationRequiredEventArgs != null)
    {
        AuthorizationAttribute attribute = AuthenticationRequiredEventArgs.RecheckAttributes();
        // verify the newly logged in account meets the requirements of the page we are visiting
        if (attribute == null)
        {
            // now we know we've passed the requirements, we can navigate to the page the user 
            // originally wanted to go to
            AuthenticationRequiredEventArgs.Frame.Navigate(AuthenticationRequiredEventArgs.UnmappedUri);
_authOp=null;
DialogResult=false; return; } if (attribute is RequiresRolesAttribute && RiaContext.Current.User.IsAuthenticated) { MessageBox.Show("This account does not have authorization to view the page you are requesting.");


DialogResult = false;
_authOp=null;



return; } } }

For this  demo we're going to display a message if you are logged in using an account that doesn't have the appropriate level of credentials. For example, if you need administrator credentials to access a page but don't currently have them we wish the login dialog to indicate that.  In the picture below, we are currently logged on under the user account, but we actually need administrative privileges, so you can see that a message is displayed to that effect:

image

The code to achieve this is pretty simple, We just display the yellow box if appropriate when the event arguments are set:

private AuthenticationRequiredEventArgs _authenticationRequiredEventArgs;
public AuthenticationRequiredEventArgs AuthenticationRequiredEventArgs
{
    get { return _authenticationRequiredEventArgs; }
    set { _authenticationRequiredEventArgs = value;
        if (AuthenticationRequiredEventArgs.AuthorizationAttribute is RequiresRolesAttribute && RiaContext.Current.User.IsAuthenticated)
        {
            _RoleRequired.Visibility = Visibility.Visible;
        }
    }
}
You can try this all out yourself by downloading the source code and making use of it in your own applications!

…Stefan

Tags:

Silverlight | .net ria services

Updates to custom routing using the Uri Mapper in Silverlight 3

by StefanOlson 18. June 2009 14:14

Update: this code is now available for download, including a project file here.

In my previous post regarding custom URI routing using the URI mapper in Silverlight 3, I described a problem where if you wish to cancel the navigation, to display a dialog for example, returning null from the UriMapper can in some scenarios cause an exception.  After some discussions with Austin Lamb, a developer on the Silverlight team, it was clear that the solution I had chosen to cancel navigation was not going to work.

So I re-thought the way I was handling this and have come up with an alternative solution which works perfectly. 

To use the CustomUriMapper, add reference to the URI mapper as below, normally in the Frame in mainpage.xaml (replacing the default one):

<Navigation2:CustomUriMapper x:Key="uriMapper" DefaultPage="/Views/EmptyPage.xaml">
<Navigation:UriMapping Uri="search/{searchfor}" MappedUri="/pages/searchpage.xaml?searchfor={searchfor}"/>
<routing:CustomUriMapper.CustomUriMappings>
<routing:AboutBoxRoute/>
<
routing:CategoryRoute/>
</
routing:CustomUriMapper.CustomUriMappings>

</
Navigation2:CustomUriMapper>

Note the new default page property, this is to provide the navigation system with a page to go to, even when canceling.  If you do not need the ability to cancel page navigation, you do not need this property. To use it just create emptypage.xaml as a blank Silverlight page class. 

If you do not need the ability to cancel page navigation - in your main page constructor, you need to tell the URI mapper about the frame in which it will be displayed:

public MainPage()
{
    InitializeComponent();
    
    (ContentFrame.UriMapper as CustomUriMapper).Frame = ContentFrame;
}

The custom routes work exactly the same as described in the previous post:

internal class CategoryRoute : CustomUriMapping
{
    public override bool MapUri(Uri uri, out Uri mappedUri)
    {
        // check for a category
        CachedCategory category = ClientStaticCache.GetCategoryByPath(uri.ToString());
        mappedUri = category != null
                        ? new Uri(string.Format("/Pages/CategoryPage.xaml?categoryid={0}", category.CategoryId),
                                  UriKind.RelativeOrAbsolute)
                        : null;
        return mappedUri != null ? true : false;
    }
}

MapUri returns a bool because there are situations where you want to return a null URI. Why would you want to do this you ask, because in some scenarios you may wish to use a URI to display a dialog, for example, an about dialog.  In this case you might have a mapping that looks like this:

internal class AboutBoxRoute: CustomUriMapping
{
    public override bool MapUri(Uri unMappedUri, out Uri mappedUri)
    {
        mappedUri = null;
        if (unMappedUri.ToString()=="About")
        {
            AboutDialog dialog = new AboutDialog();
            dialog.Show();
            return true; // we are not going to go anywhere, just let the dialog display
        }
        mappedUri = null;
        return false;
    }
}

The new CustomUriMapper is shown below.  The code is be available for download hereor in my next post, which describes the new authenticating URI mapper, a subclass of the CustomUriMapper.

/// <summary>
/// Custom URI mapper provides support for custom URI mapping using code, rather than just using regular expressions, 
/// which is the only support provided by the Microsoft URI mapper.  it also provides support for canceling navigation
/// if you wish to display a dialog when a URI is specified
/// </summary>
/// <example>
/// There are two parts to using the custom URI mapper. Firstly, you declare the URI mapper and app.xaml as you would the
/// normal Microsoft mapper. If you wish to be able to cancel navigation, you can set a default page, in app.xaml. Secondly, 
/// if you wish to be able to cancel navigation to display a dialog or anything else, you need to set the frame.
/// 
/// In app.xaml:
/// <code>
/// <routing:CustomUriMapper x:Key="uriMapper" DefaultPage="/Views/EmptyPage.xaml">
//      <Navigation:UriMapping Uri="search/{searchfor}" MappedUri="/pages/searchpage.xaml?searchfor={searchfor}"/>
//  </routing:CustomUriMapper>
/// </code>
/// 
/// To set the frame, probably in the main window.
/// <code>
/// (App.Current.Resources["uriMapper"] as CustomUriMapper).Frame = frame;
/// </code>
/// 
/// </example>
[ContentProperty("UriMappings")]
public class CustomUriMapper : UriMapperBase
{
    // private variables
    private bool _cancelNavigation;

    /// <summary>
    /// Gets or sets a list of UriMapping objects.
    /// </summary>
    public Collection<UriMapping> UriMappings { get; private set; }
    /// <summary>
    /// Gets or sets a list of CustomUriMappings objects.
    /// </summary>
    public Collection<CustomUriMapping> CustomUriMappings { get; private set; }

    /// <summary>
    /// specifies the default page. This is required because even when canceling navigation, the Microsoft URI mapper
    /// needs to provide a page to map to. To use this create a blank page and reference it.
    /// </summary>
    public Uri DefaultPage { get; set; }

    private Frame _frame;
    /// <summary>
    /// specifies the frame in which navigation for this URI mapper will occur. If you want to be able to cancel 
    /// navigation, this frame needs to be set.
    /// </summary>
    /// <example>
    /// To set the frame, probably in the main window.
    /// <code>
    /// (App.Current.Resources["uriMapper"] as CustomUriMapper).Frame = frame;
    /// </code>
    /// </example>
    public Frame Frame
    {
        get { return _frame; }
        set
        {
            _frame = value;
            _frame.Navigating += FrameNavigating;
        }
    }

    public CustomUriMapper()
    {
        UriMappings = new Collection<UriMapping>();
        CustomUriMappings = new Collection<CustomUriMapping>();
    }
    
    /// <summary>
    /// Maps a given URI and returns a mapped URI.
    /// </summary>
    /// <param name="uri">Original URI value to be mapped to a new URI.</param>
    /// <returns>A URI derived from the <paramref name="uri"/> parameter.</returns>
    public override Uri MapUri(Uri uri)
    {
        _cancelNavigation = false;

        Collection<UriMapping> uriMappings = UriMappings;
        if (uriMappings == null)
        {
            throw new InvalidOperationException("MustNotHaveANullUriMappingCollection");
        }
        foreach (UriMapping mapping in uriMappings)
        {
            Uri uri2 = mapping.MapUri(uri);
            if (uri2 != null)
            {
                return CheckCanNavigateToUri(uri, uri2);
            }
        }

        foreach (CustomUriMapping mapping in CustomUriMappings)
        {
            Uri uri2;
            if (!mapping.MapUri(uri, out uri2)) continue;
            if (uri2 == null && Frame!=null)
            {
                return CancelNavigation();
            }
            return CheckCanNavigateToUri(uri, uri2);
        }
        // now nothing...
        return CheckCanNavigateToUri(uri, uri); 
    }

    /// <summary>
    /// cancels navigation to the current URI.
    /// </summary>
    /// <exception cref="InvalidOperationException">
    /// thrown if there is no default page
    /// </exception>
    /// <returns>the new URI to navigate to, which will be the default page. This is required by 
    /// the Microsoft navigation system</returns>
    protected Uri CancelNavigation()
    {
        // if there is no URI to go to, we will cancel the navigation at the next possible opportunity
        _cancelNavigation = true;
        if (DefaultPage==null)
        {
            throw new InvalidOperationException("must have a default page");
        }
        // we need to return a valid URI, that points to the actual page, because the cancel is not 
        // checked until after the validity of the URI is checked
        return DefaultPage;
    }

    /// <summary>
    /// this function is called before any URI is returned. This gives you the opportunity to do any custom work, 
    /// such as authentication, prior to the URI being returned to the navigation system
    /// </summary>
    /// <param name="unmappedUri">the URI that was provided by the navigation system, before it was mapped</param>
    /// <param name="mappedUri">the URI, after mapping</param>
    /// <returns>the URI to navigate to, by default this is the same as the mapped URI</returns>
    protected virtual Uri CheckCanNavigateToUri(Uri unmappedUri, Uri mappedUri)
    {
        return mappedUri;
    }

    void FrameNavigating(object sender, NavigatingCancelEventArgs e)
    {
        e.Cancel = _cancelNavigation;
    }
}
…Stefan

Tags:

Silverlight

Issues with the Silverlight Navigation architecture

by StefanOlson 3. June 2009 15:56

In my last post, I described some of the ways I was able to use the Silverlight URI mapping architecture to provide more customized routing.

However, there are a few problems with the navigation/URI mapping architecture that I haven't been able to find ways to work around.

Setting a title

One of the major problems I've encountered is getting the title (in the browser's title bar) to be set. There is a field in the Page class called title. So you would think would be relatively simple. It appears that there's some difficulty in setting the page title, unless it's set at a particular time during the navigation. Here's how I try and set it:

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    CachedCategory category = ClientStaticCache.GetCategoryById(
int.Parse(NavigationContext.QueryString["categoryid"])); Title = category.Name + " - " + "my site"; }

 

But the title never appears -  the URI Appears as the title. I started a thread on the Silverlight forum, but no one has been able to give me an answer as to when you can set the title so that it appears correctly in the title bar and the navigation history of the browser.

When you're working in an asynchronous world you often won't get the title at the time the navigation starts, there needs to be a way to set the title when the data has come down the line.

Asynchronous operation

If I want to go and just find out if a particular URI exists (e.g: a category), before navigating to it, I either have to have a cache of categories on my local Silverlight client, which is what I'm doing at the moment, or you need to go to the server and verify that that is a valid category. The problem with the URI mapping architecture is that it is synchronous, so it is not possible to go and ask the server and then come back to the URI mapper and say yes, this is where I want to go.

Null return from the URI mapper causes exception

As described in my previous post, if you return null from the URI mapper because you've handled the URI by displaying a dialog or something like that, an exception is caused. However, the exception doesn't happen if you change the URI in the address bar, only if you call it via Frame.Navigate.

Bug on connect

Reusing a page

Another scenario occurs when we have we have content on a page which is very slow to load. In my case the Virtual Tour Viewer has a page that contains floor plans. These are very slow to load, and I don't want to have to reload the floor plans as the page changes, because some pages display the floor plans and others don't.

Ideally, the Frame system would be able to come to you and say here's a page I'm asking for, would you like to give me the page. In this case, I could reuse the pages containing the floor plan and increase the speed of the application simply by handling an event on the frame.  e.g:

Page Frame_GetPageForUri(object sender, GetPageForUriEventArgs e)
{
    if (e.Uri.ToString() == "categorycontrol.xaml")
    {
        return CategoryControl;
    }
    return null;
}

private Page _CategoryControl;

private Page CategoryControl
{
    get
    {
        if (_CategoryControl == null)
        {
            _CategoryControl = new CategoryControl();
        }
        return _CategoryControl;
    }
}

The way I am looking to do this currently is to store the floor plan control outside of the page so that it doesn't get destroyed if the garbage collector tries to clean up when the page isn't displayed, but that is not very satisfactory.

Role-based pages

Another useful piece of the navigation architecture would be the ability to use an attribute to declare the required roles to display a particular page. If the user does not have that role, then a login dialog would be displayed, in the same way as is done with the ASP.net authentication system. Under Silverlight, you could do this in the same way as is done on domain service you have a requires authentication attribute and a requires role attribute:

[RequiresAuthentication]
public partial class CategoryPage : Page

WPF compatibility

The virtual tour viewer needs to work on both WPF and Silverlight. I've encountered a number of problems trying to share the code between WPF and Silverlight with regard to the navigation framework.  WPF 4 currently doesn't have the API improvements that have been made to the Silverlight navigation framework such as the URI mapper and a number of virtual functions in the page class.  This means I have had to create a whole set of classes just to create a level of compatibility, part of which is completely impossible because of the lack of the URI mapper!

It's not clear at this stage of this will be fixed by Beta 2 of the .net framework 4.

Overall the Silverlight navigation API is pretty good,  Hopefully some minor tweaks will make it a huge amount better.

Tags:

Silverlight | WPF

Custom routing using the Uri Mapper in Silverlight 3

by StefanOlson 3. June 2009 15:34

Update: see the latest version of this code here.

The new navigation architecture in Silverlight 3 is very exciting as it substantially improves the viability of Silverlight for use replacing html websites (except for the lack of a hyperlink class, which I can't understand not being included in Silverlight 2). I'm currently developing a website which will be running Silverlight 3 and have discovered some interesting issues with the URI mapping architecture as it currently stands in the beta.

The first thing I wanted to do with the URI mapping architecture was to add my own routing system where I could examine the URI and decide which page to send it to.  Out of the box the Silverlight has a Uri Mapper class that allows you to use regular expressions to convert the display URI into the URI you wish to use.  For example (in App.xaml):

<Navigation1:UriMapper x:Name="_uriMapper">
    <Navigation1:UriMapping Uri="About" MappedUri="/Views/About.xaml"/>
    <Navigation1:UriMapping Uri="About/{person}/" MappedUri="/Views/About.xaml?person={person}"/>
</Navigation1:UriMapper>

Unfortunately with the way that the URI mapping is structured at the moment, the only way to do this in code, rather than in xaml is to completely duplicate the UriMapper class and create your own specialized type of mapping. Ideally, there should be a UriMappingBase class and UriMapping should be derived from that. The classes that I have developed for this are shown below:

[ContentProperty("UriMappings")]
public class CustomUriMapper : UriMapperBase
{
    // Methods
    public CustomUriMapper()
    {
        UriMappings = new Collection<UriMapping>();
        CustomUriMappings = new Collection<CustomUriMapping>();
    }

    public override Uri MapUri(Uri uri)
    {
        Collection<UriMapping> uriMappings = UriMappings;
        if (uriMappings == null)
        {
            throw new InvalidOperationException("MustNotHaveANullUriMappingCollection");
        }
        foreach (UriMapping mapping in uriMappings)
        {
            Uri uri2 = mapping.MapUri(uri);
            if (uri2 != null)
            {
                return uri2;
            }
        }

        foreach (CustomUriMapping mapping in CustomUriMappings)
        {
            Uri uri2;
            if (mapping.MapUri(uri, out uri2))
            {
                return uri2;
            }
        }
        // now nothing...
        return uri;
    }


    // Properties
    public Collection<UriMapping> UriMappings { get; private set; }
    public Collection<CustomUriMapping> CustomUriMappings { get; private set; }
}

public abstract class CustomUriMapping
{
    public abstract bool MapUri(Uri unMappedUri, out Uri mappedUri);
}

This allows me to create my own custom routes so that if I need to do some custom analysis of the URI before I decide which page to use, this can be done, as shown below:

internal class CategoryRoute : CustomUriMapping
{
    public override bool MapUri(Uri uri, out Uri mappedUri)
    {
        // check for a category
        CachedCategory category = ClientStaticCache.GetCategoryByPath(uri.ToString());
        mappedUri = category != null
                        ? new Uri(string.Format("/Pages/CategoryPage.xaml?categoryid={0}", category.CategoryId),
                                  UriKind.RelativeOrAbsolute)
                        : null;
        return mappedUri != null ? true : false;
    }
}

MapUri returns a bool because there are situations where you want to return a null URI. Why would you want to do this you ask, because in some scenarios you may wish to use a URI to display a dialog, for example, an about dialog.  In this case you might have a mapping that looks like this:

internal class AboutBoxRoute: CustomUriMapping
{
    public override bool MapUri(Uri unMappedUri, out Uri mappedUri)
    {
        mappedUri = null;
        if (unMappedUri.ToString()=="About")
        {
            AboutDialog dialog = new AboutDialog();
            dialog.Show();
            return true; // we are not going to go anywhere, just let the dialog display
        }
        mappedUri = null;
        return false;
    }
}

If the unmapped URI was equal to “About”, the about dialog would display and null would be returned from the UriMapper, so no navigation would occur . Unfortunately there is a bug in the Silverlight beta, which means that returning null from the UriMapper can in some scenarios cause an exception. I’ve filed a bug on connect for this and hopefully it will be fixed before RTW.

Adding the custom URI mapper to your project is the same as adding the Microsoft one, except that you reference a different class.

<routing:CustomUriMapper x:Key="uriMapper">
    <Navigation:UriMapping Uri="search/{searchfor}" MappedUri="/pages/searchpage.xaml?searchfor={searchfor}"/>
<routing:CustomUriMapper.CustomUriMappings> <routing:AboutBoxRoute/> <routing:CategoryRoute/> </<routing:CustomUriMapper.CustomUriMappings> </routing:CustomUriMapper>

As you can see, you can add any custom routes directly in the xaml.

So there are some solutions for some issues with the URI mapping as currently implemented in Silverlight 3. In my next post I will explain some of the issues that I haven't been able to work around.

Tags:

Silverlight

About the author

Stefan Olson is the Managing Director of Olson Software.  He has been developing software using Microsoft Technologies for nearly 20 years.

He is currently working on building the next generation Virtual Tour software in WPF and Silverlight for www.palacevirtualtours.com.

Tag cloud