Search code examples
silverlightattached-properties

AttachedProperty in XAML firing before I can authenticate & determine role


Using VS2008, Silverlight 4.

I have created an AttachedProperty, "RequiresRole", that I'm setting in my XAML. If a user isn't in the role defined by that property, the visibility of said control is set to collapsed.

The problem I'm currently having is that the 'UserHasRole' check (within my Authorization class, as part of the RequiredRole_Callback function) is firing when I load the application, but before I have a chance to login, authenticate, and set the Role for the user. So the property is working, because the menu item is not visible, but it's not currently alerting my UI, after authentication, that the role has changed. I know where to implement it, just not how. INotifyPropertyChanged? Not sure how that'd fit.

I have a few properties in my App.xaml.cs, "IsAuthenticated", which is set to true when a user logs in (using a service to call a SPROC), and "Role", which is the return value of a successful call to Logon SPROC.

Relevant Authorization.cs code:

public static readonly DependencyProperty RequiredRoleProperty = DependencyProperty.RegisterAttached(
        "RequiredRole", 
        typeof(string), 
        typeof(Authorization), 
        new PropertyMetadata(RequiredRole_Callback));

    // This callback will be invoked when some control will receive a value for your 'RequiredRole' property
    private static void RequiredRole_Callback(DependencyObject source, DependencyPropertyChangedEventArgs e)
    {
        var uiElement = (UIElement)source;
        RecalculateControlVisibility(uiElement);

        // also this class should subscribe somehow to role changes and update all control's visibility after role being changed
    }

    private static void RecalculateControlVisibility(UIElement control)
    {
        //Authorization.UserHasRole() - is your code to check roles
        if (Authorization.UserHasRole(GetRequiredRole(control)))
            control.Visibility = Visibility.Visible;
        else
            control.Visibility = Visibility.Collapsed;
    }

    private static Boolean UserHasRole(string role)
    {
        string Role = App.Role;
        if (Role.ToLower() == role.ToLower())
        {
            return true;
        }
        return false;
    }

Relevant MainPage.xaml code:

xmlns:s="clr-namespace:TSMVVM.Authorization"

<HyperlinkButton x:Name="lnkLogin" 
HorizontalAlignment="Right" 
Tag="Login"
Command="{Binding NavigateCommand}"
s:Authorization.RequiredRole="Azdmin"
CommandParameter="{Binding Tag, ElementName=lnkLogin}"
Content="Login" />

Relevant Mainpage.xaml.cs code:

void MainPage_Loaded(object sender, RoutedEventArgs e)
    {
        if (!App.IsAuthenticated)
        {
            ContentFrame.Navigate(new Uri("Login", UriKind.Relative));
        }
    }

Updated Code (Note, changed RequiredRole to RequiresRole) Authorization.cs

public class Authorization
{
    #region Attached DP registration

    public static string GetRequiresRole(UIElement obj)
    {
        return (string)obj.GetValue(RequiresRoleProperty);
    }
    public static void SetRequiresRole(UIElement obj, string value)
    {
        obj.SetValue(RequiresRoleProperty, value);
    }

    #endregion

    /// Using a DependencyProperty as the backing store for requiresRole.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty RequiresRoleProperty = DependencyProperty.RegisterAttached(
        "RequiresRole", 
        typeof(string), 
        typeof(Authorization), 
        new PropertyMetadata(RequiresRole_Callback));
    // This callback will be invoked when some control will receive a value for your 'RequiredRole' property
    private static void RequiresRole_Callback(DependencyObject source, DependencyPropertyChangedEventArgs e)
    {
        var uiElement = (UIElement)source;

        if (App.IsAuthenticated)
        {
            RecalculateControlVisibility(uiElement);
        }
        else
        {
            EventHandler eh = null;
            eh = delegate
            {
                RecalculateControlVisibility(uiElement);
                ((App)Application.Current).Authenticated -= eh;
            };
            ((App)Application.Current).Authenticated += eh;
        }
    }


    private static void RecalculateControlVisibility(UIElement control)
    {
        //Authorization.UserHasRole() - is your code to check roles
        if (Authorization.UserHasRole(GetRequiresRole(control)))
            control.Visibility = Visibility.Visible;
        else
            control.Visibility = Visibility.Collapsed;
    }

    private static Boolean UserHasRole(string role)
    {
            if (App.Role != null)
            {
                string Role = App.Role[0].ToString();
                if (Role.ToLower() == role.ToLower())
            {
                return true;
            }
        }
        return false;
    }
}

App.xaml.cs

private static string _role;
public event EventHandler Authenticated = delegate { };

public static string Role
{
    get
    {
        return _role;
    }
    set
    {
        _role = value;
    }
}

MainPage.xaml:

<HyperlinkButton x:Name="lnkSiteParameterDefinitions" 
Style="{StaticResource LinkStyle}" 
Tag="SiteParameterDefinitions" 
s:Authorization.RequiresRole="Admin"
Content="Site Parameter Definitions" 
Command="{Binding NavigateCommand}"
CommandParameter="{Binding Tag, ElementName=lnkSiteParameterDefinitions}"/>

And finally, login.xaml.cs

private void LogonService_LogonCompleted(object sender, TSMVVMLogonSVC.LogonCompletedEventArgs e)
    {
        ObservableCollection<string> Result = e.Result;
        switch (Result[0])
        {
            case "Logon Failed":
                break;

            case "Logon Successful":
                PageConductor = new Services.PageConductor();
                App.CurrentUser = Result[2];
                App.Role = Result[1].ToString();
                App.IsAuthenticated = true;
                Messenger.Default.Send(new TSMVVM.Messages.FrameMessage() { RootFrame = (Frame)this.Parent});
                PageConductor.GoToView("Home", "main");
                break;

            case "Change Password":
                break;
        }

    }

Solution

  • I don't think INotifyPropertyChanged is really what you need in this case. You just need a one shot event when the application has authenticated.

    Add an simple event to your App class:-

     public event EventHandler Authenticated = delegate { };
    

    Now modify your call back:-

    private static void RequiredRole_Callback(DependencyObject source, DependencyPropertyChangedEventArgs e)
    {
         var uiElement = (UIElement)source;
    
         if (App.IsAuthenticated)
         {
             RecalculateControlVisibility(uiElement);
         }
         else
         {
              EventHandler eh = null;
              eh = delegate
              {
                   RecalculateControlVisibility(uiElement);
                   ((App)Application.Current).Authenticated -= eh;
              };
              ((App)Application.Current).Authenticated += eh;
         }
    }
    

    Note the removal of the event handler after it has fired is very important, without it you may well have a memory leak.