Tim Heuer published a great example on how to implement user authentication and role validation with ASP.NET Application Services in his Application Corner a while ago. It really works as expected, even with custom membership / role providers. And you get to do declarative role based security checks on your server side classes too.
When building MVVM pattern applications, you tend to have as less code as possible in your views’ code-behind classes. Call me a MVVM purist [ :), see Tim’s video interview for pixel8 ], but (close to) zero lines of custom code is what I like to have in my code-behinds. Not at all costs, but usually it doesn’t take much to code additional helper class, an attached property or a controller of some sort.
What I wanted to do, was replace the following piece of code with something more declarative, which could be used in Xaml:
private void EnableAppFeatures(ObservableCollection<string> roles)
{
EmployeeButton.IsEnabled = (roles.Contains("Admin") || roles.Contains("HR"));
OrderButton.IsEnabled = (roles.Contains("Admin") || roles.Contains("Sales"));
CustomersButton.IsEnabled = (roles.Contains("Admin") || roles.Contains("Sales"));
ReportsButton.IsEnabled = (roles.Contains("Admin") || roles.Contains("Sales"));
}
[the roles string collection comes in as a result of the Roles service call]
Solution #1 - Good
If you’re using MVVM, adding some additional properties to your ViewModels (like IsInHRRole, IsInSalesRole, etc) should be easy, right? You would just need to bind al the buttons’ IsEnabled properties to their ViewModel counterparts and you’re done.
<Button ... x:Name="ReportsButton" ... IsEnabled="{Binding IsInSalesRole}">
True, but you’ll probably end up “polluting” all your ViewModels with such properties since usually the majority of the Views have some elements, depending on a role the user is in. Additionally, you’d have to create additional properties for combined roles, e.g. IsInSalesOrAdminRole,
Solution #2 - Better
I was thinking somewhere in the lines of attaching a new attached property to each button, which would enable the button when the user would be in one of the specified roles:
<Button ... x:Name="ReportsButton" ... sec:SecurityAction.DemandRole="Sales,Admin">
SecurityAction.DemandRole in this case is an attached property, specifying all the roles, which would make the button enabled. The logic is simple – if the user is in one of the specified roles, the button (or any other control with this attribute appended) would have its IsEnabled property set to true.
To make this possible, I first had to come up with a class to hold all the current roles, something like a
Principal… This is a short class, supporting roles only that will do a job for this demonstration:
public class SilverlightPrincipal
{
public static ObservableCollection<string> Roles { get; private set; }
static SilverlightPrincipal()
{
Roles = new ObservableCollection<string>();
}
public static void SetRoles(IList<string> roles)
{
Roles.Clear();
for (int i = 0; i < roles.Count; i++)
{
Roles.Add(roles[i]);
}
}
public static bool IsInRole(string role)
{
return Roles.Contains(role);
}
public static bool IsInOneOfRoles(string[] roles)
{
for (int i = 0; i < roles.Length; i++)
{
if (Roles.Contains(roles[i]))
{
return true;
}
}
return false;
}
}
The roles collection will contain all roles the user is currently in. IsInRole() and IsInOneOfRoles() will return true, if user is in one of the passed in roles. The SetRoles() method is here for simplicity – it servers just as a roles setter, called from wherever the Roles service calls returns.
Perhaps the class above probably doesn’t even deserve to be called a Principal, but it will serve well for the purpose of this post. For more ideas to implement a more complete Silverlight Principal, check this blog post.
To plug it in Tim’s code, I used the aforementioned EnableAppFeatures method, which now looks like:
private void EnableAppFeatures(ObservableCollection<string> roles)
{
SilverlightPrincipal.SetRoles(roles);
}
The next thing to do was create the SecurityAction.DemandRole attached property that could be used to set the required roles to a specific control. You’ll find this attached property in the sample code attached to this post. I’m not including the code here because it will make this post much longer and I’m not really fond of using it anyway, because of the reasons for solution #3 below. However, feel free to try the property and please leave comment if you like (or dislike) it.
Solution #3 - Best
After creating #2, I thought – if user can never access the functionality behind the buttons, there is no need to show the buttons to her in the first place, right? Why not showing her the dashboard, created specifically for her role? Sounds familiar? ASP.NET? LoginView?
Here’s my simple LoginView control for Silverlight. You can define as many LoginView templates for as many roles you have:
<controls:LoginView>
<controls:LoginView.Templates>
<controls:LoginViewDataTemplate Roles="HR”>
<!-- Layout for HR role -->
</controls:LoginViewDataTemplate>
<controls:LoginViewDataTemplate Roles="Sales”>
<!-- Layout for Sales role -->
</controls:LoginViewDataTemplate>
<controls:LoginViewDataTemplate Roles="Admin”>
<!-- Layout for Admin role -->
</controls:LoginViewDataTemplate>
</controls:LoginView.Templates>
</controls:LoginView>
First, here’s the LoginViewTemplate class:
public class LoginViewDataTemplate : DataTemplate
{
public string Roles { get; set; }
}
Yeah, that’s pretty much it. The Roles property will hold the list of roles, which are allowed to see the template.
The LoginView control itself is simple too:
public class LoginView : UserControl
{
public Collection<LoginViewDataTemplate> Templates { get; set; }
public LoginView()
{
Templates = new Collection<LoginViewDataTemplate>();
SilverlightPrincipal.RegisterForRoleChangeNotification(this, LoadTemplate);
Loaded += (s,e) => { LoadTemplate(); };
}
private void LoadTemplate()
{
for (int i = 0; i < Templates.Count; i++)
{
string[] roleNames = Templates[i].Roles.Split(new char[] { ',', ' ' },
StringSplitOptions.RemoveEmptyEntries);
bool isInRole = SilverlightPrincipal.IsInOneOfRoles(roleNames);
if (isInRole)
{
Content = Templates[i].LoadContent() as UIElement;
return;
}
}
Content = null;
}
}
[Update 06-13: updated the above code; should work now]
There’s not much to comment here… The Templates collection holds the collection of, errr., LoginViewDataTemplates, and the LoadTemplate method is there to Load the first template, having one of the specified roles match the current role that user is in. This could be easily extended to support the default template, if needed. The default template would be loaded when no other template would match the current user’s role.
RoleChanged notification /execution
The right template is loaded with the control and whenever the Roles might change, the control would reload the template. I’ve implemented this behavior by adding a registration method for role change notification. Any visual element (or whatever object for that matter) can register itself by passing in the delegate to whatever method should be called whenever role(s) would change. The registration method was conveniently added to existing SilverlightPrincipal class:
public static void RegisterForRoleChangeNotification(object o, Action action)
{
bool existsInCollection = references.Keys.FirstOrDefault(reference => reference.Target == o) != null;
if (!existsInCollection)
{
references.Add(new WeakReference(o), action);
}
}
Whenever the roles would change, register objects’ delegate would be called:
private static void OnRolesChanged()
{
CleanUp();
foreach (var reference in references)
{
reference.Value();
}
}
The CleanUp method takes care for dead/disposed objects:
private static void CleanUp()
{
List<WeakReference> list = references.Keys.ToList<WeakReference>();
for (int i = list.Count - 1; i >= 0; i--)
{
WeakReference r = list[i];
if (!r.IsAlive)
{
references.Remove(r);
}
}
}
Classes, included in the package: SilverlightPrincipal, SecurityAction, LoginView: