Unlike MVC or Web Forms, when using WPF, you don't get a pre-built security system. Thus, you need to come up with your own method of securing controls on WPF screens, such as the one shown in Figure 1. There are a few different methods to accomplish this goal. For example, you can create different properties in your View model to make controls invisible or disabled based on a user's role. The problem with this approach is that if you need to secure more controls, you need to change the code and then redistribute your WPF application to your users. In this article, I'm going to take a data-driven approach to security so you can make changes in a database table and have your WPF application's security update without having to make code changes.
Build an Employee Project
To get the most out of this article, I suggest you build the sample as you read. Create a new WPF application using Visual Studio named WPFSecuritySample
. Add a new folder namedUserControls
. Right mouse-click on this folder and add a user control named EmployeeControl
. This control is where you're going to create the screen shown in Figure 1.
Employee User Control
Create the employee user control by typing in the code shown in Listing 1. In this screen, you're adding the Name
property to some of the controls and the Tag
property to others. This is to illustrate that you can use either of these properties to locate the control you wish to secure. Later in this article, you're going to use the {Binding Path} expression to locate the control to secure as well.
Listing 1: The XAML for the Employee screen
<UserControl x:Class="WPFSecuritySample.EmployeeControl" ... // NAMESPACES HERE >
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="1" HorizontalAlignment="Left" Content="New" Name="NewButton" />
<Label Grid.Row="1" Grid.Column="0" Content="Employee ID" />
<TextBox Grid.Row="1" Grid.Column="1" Name="EmployeeID" Text="{Binding Path=EmployeeID}" />
<Label Grid.Row="2" Grid.Column="0" Content="First Name" />
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Path=FirstName}" />
<Label Grid.Row="3" Grid.Column="0" Content="Last Name" />
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding Path=LastName}" />
<Label Grid.Row="4" Grid.Column="0" Content="Salary" />
<TextBox Grid.Row="4" Grid.Column="1" Tag="Salary" Text="{Binding Path=Salary}" />
<Label Grid.Row="5" Grid.Column="0" Content="SSN" />
<TextBox Grid.Row="5" Grid.Column="1" Tag="SSN" Text="{Binding Path=SSN}" />
<Button Grid.Row="6" Grid.Column="1" HorizontalAlignment="Left" Content="Save" Name="SaveButton" />
</Grid>
</UserControl>
SecurityViewModelBase Class
When creating WPF applications, you should always use the Model-View-ViewModel (MVVM) design pattern. You're going to create an EmployeeViewModel
class in the next section, but first, create a base class named SecurityViewModelBase
where you write code to secure controls. The EmployeeViewModel
class inherits from the SecurityViewModelBase
class.
Add a new folder to the project named SecurityClasses
. Right mouse-click on this folder and add a new class named SecurityViewModelBase
. Add the code shown in Listing 2 to this new file. This code is just the stubbed methods you're going to write a little later in this article, but you need to create them now so you can build the employee view model. A short description of the properties and methods you see in Listing 2 are described in Table 1.
Listing 2: Create a base class for all view models that need security to inherit from
using System.Collections.Generic;
using System.Security.Principal;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
public class SecurityViewModelBase
{
public SecurityViewModelBase()
{
ControlsToSecure = new List<SecurityControl>();
ControlsInContainer = new List<XAMLControlInfo>();
}
public List<SecurityControl> ControlsToSecure { get; set; }
public List<XAMLControlInfo> ControlsInContainer { get; set; }
public IPrincipal CurrentPrincipal { get; set; }
public virtual void SecureControls(object element, string containerName) { }
protected virtual void LoadControlsToSecure(string containerName) { }
protected virtual void LoadControlsInXAMLContainer(object element) { }
}
Employee View Model
Create a new folder in your project named ViewModelClasses
. Right mouse-click on that folder and add a new class named EmployeeViewModel
. Add the code shown in Listing 3 to this new file. The EmployeeViewModel
class inherits from the SecurityViewModelBase
class so it can take advantage of the security methods.
Listing 3: Make all your view models inherit from the SecurityViewModelBase class
using System.Collections.Generic;
public class EmployeeViewModel : SecurityViewModelBase
{
public EmployeeViewModel() : base()
{
EmployeeID = 1;
FirstName = "Bruce";
LastName = "Jones";
Salary = 75000;
SSN = "555-55-5555";
}
public int EmployeeID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public decimal Salary { get; set; }
public string SSN { get; set; }
protected override void LoadControlsToSecure(string containerName) { }
}
In the EmployeeViewModel
class, add the individual properties to bind to the employee screen. Override the LoadControlsToSecure()
method so you can choose from where to retrieve the controls to secure. For example, you may hard-code the controls, or retrieve them from an XML file or a database table.
Main Window
Now that you have your view model classes created, drag the EmployeeControl
user control onto the MainWindow
. Be sure to reset all layout properties so the user control takes up the full width of your window. Your <Grid>
in this window should look like the following:
<Grid>
<UserControls:EmployeeControl />
</Grid>
Add an XML namespace to the <Window>
control so you can create an instance of the EmployeeViewModel
class within the resources section of the window control.
xmlns:vm="clr-namespace:WPFSecuritySample.ViewModels"
Add a <Window.Resources>
section where you can create the definition for the employee view model, as shown in the code snippet below.
<Window.Resources>
<vm:EmployeeViewModel x:Key="viewModel" />
</Window.Resources>
Assign the DataContext attribute of the <Grid>
to the instance of this view model class, as identified by the key “viewModel”.
<Grid DataContext="{StaticResource viewModel}">
<UserControls:EmployeeControl />
</Grid>
Main Window Code-Behind
Go to the code-behind for the MainWindow
class and add a private variable named _viewModel
. Connect this variable to the instance of the view model created by XAML using the code below.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
_viewModel = (EmployeeViewModel)
this.Resources["viewModel"];
}
private readonly EmployeeViewModel _viewModel;
}
Set Security Principal Object
In order to secure your controls on each window, you need to add a Loaded
event to your window. From this event, call the SecureControls()
method on your view model class. Open the MainWindow.xaml
file and add the following key/value pair in your <Window>
to create the Loaded
event.
Loaded="Window_Loaded"
Listing 4 shows both the Window_Loaded()
event procedure and the SetSecurityPrincipal()
method. The SetSecurityPrincipal()
method is where you need to create, or get, an IPrincipal
object and change the principal policy on the current thread of execution. You can either grab the current WindowsPrincipal
object as shown here, or you can create your own GenericPrincipal
object by prompting a user for their username and password.
Listing 4: Set a security principal on the thread before you attempt to secure the controls
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// Set your Security Principal
SetSecurityPrincipal();
// Secure controls on this WPF window
_viewModel.SecureControls(this, "EmployeeControl");
}
private void SetSecurityPrincipal()
{
// Set Principal to a WindowsPrincipal
Thread.GetDomain().SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
// NOTE: You can create a GenericPrincipal here with your own credentials and roles
}
Call the SetSecurityPrincipal()
method from within the Window_Loaded()
event. After the call to this method, call the SecureControls()
method on the view model passing in the instance of this window and the name of the user control as a string.
Try It Out
Run the application and you should see a screen that looks like Figure 1. You're now ready to start building the part of the code that secures the various controls on your screen.
Goals of a Security System
Before you write the code to implement a security system, let's determine the features you need in a security system. The most basic feature is the ability to make a control appear or disappear based on a role. Of course, visibility in WPF can mean either hidden or collapsed, so you most likely want to be able to specify either. The ability to make a control become disabled based on a role should be another feature in your security system. Finally, you might also want to make input controls, such as text boxes, become read-only for certain roles.
Prepare Screen for Security
You don't want to have to hard-code the security on each individual screen or within each view model class. Instead, a data-driven approach is a better choice. The way to accomplish this is to pass in a reference to a XAML element such as a Window, a User Control, a Grid, or other container to the SecureControls()
method. All of the controls contained within this container are read in and a unique identifier for each control is located. This unique identifier can be the Name or Tag properties, or maybe the property name in the {Binding Path="propertyName"}
expression. The XAML shown below highlights the button and text box you're going to secure on the employee screen. Each control you want to secure must have a Name, Tag, or a {Binding} expression that's unique.
<Button Content="New" Name="NewButton" />
<TextBox Name="EmployeeID" Text="{Binding Path=EmployeeID}" />
<TextBox Tag="Salary" Text="{Binding Path=Salary}" />
<TextBox Tag="SSN" Text="{Binding Path=SSN}" />
<Button Content="Save" Name="SaveButton" />
Collection of Security Objects
Table 2 shows a sample set of security data stored in a collection of SecurityControl
objects. This SecurityControl
collection secures the controls shown in Figure 1. For each control, specify what Mode
you wish that control to take on, such as read-only, collapsed, hidden, or disabled. The Roles property contains a string array of roles. If the user does NOT belong to one of those roles, the value in the Mode
property is used to change the state of the control. For example, if the user isn't in a “Users” or “Supervisors” role, the Visibility property of the control identified as NewButton is set to “Collapsed”.
For the employee screen shown in Figure 1, you don't want to allow normal users of the system to be able to save any changes to employee data. Thus, hide the “New” button and disable the “Save” button on the screen. You also don't want normal users to see the salary of other employees, so make the salary text box invisible. An administrator in your application has the right to save employee data and to see the salary data.
To match this set of data to the elements on your WPF Window or User Control, the ElementIdentifier
property can be the Name
or Tag
property of the control to secure, or the value in the {Binding} expressions' Path
property. The ContainerName property (used later in this article) is the name of the XAML container you wish to secure.
To keep things simple, you're going to create a hard-coded collection of SecurityControl
objects with element identifiers to match the controls in the EmployeeControl
user control in your project, as shown in Figure 2. For now, you're also just going to use the Name
or Tag
properties to identify each control.
WPF Security Classes
You've already created the stub for the SecurityViewModelBase
class, and the EmployeeViewModel
class that inherits from it. There are two additional classes (Figure 3) you need to create in order to be able to secure controls on any WPF screen. The first class is named XAMLControlInfo
and is used to hold information about controls within the WPF container you wish to secure. The second control is the SecurityControl
class, which is the class to hold the information described in Table 2.
Both classes are added as List<T>
type properties in the SecurityViewModel
class. The methods LoadControlsToSecure()
and LoadControlsInXAMLContainer()
are responsible for loading each of these generic collection classes.
SecurityControl Class
The SecurityControl
class (Listing 5) holds the data to secure a single control on a Window or User Control or other WPF container control. Each row of data shown in Table 2 is created by placing security data into a new instance of a SecurityControl
class. The Roles
property is a string array, but there's also a RolesAsString
property that's used to express that Roles
array as a comma-delimited list, if needed.
Listing 5: The SecurityControl class holds security information about a single control
public class SecurityControl
{
public string ContainerName { get; set; }
public string ElementIdentifier { get; set; }
public string Mode { get; set; }
public string[] Roles { get; set; }
private string _RolesAsString = string.Empty;
public string RolesAsString
{
get { return _RolesAsString; }
set
{
_RolesAsString = value;
Roles = _RolesAsString.Split(',');
}
}
}
XAMLControlInfo Class
Once you have the list of controls to secure, you need to gather a list of the controls on the WPF element you wish to secure. The WPF element can be a Window, a User Control, or a XAML container control such as a Grid or a StackPanel. The XAMLControlInfo
class (Listing 6) is the one that holds the information about each control within the WPF element.
Listing 6: The XAMLControlInfo class holds information about a control within a WPF container
public class XAMLControlInfo
{
public object TheControl { get; set; }
public string ControlName { get; set; }
public string Tag { get; set; }
public string ControlType { get; set; }
public bool HasIsReadOnlyProperty { get; set; }
public bool ConsiderForSecurity()
{
return !string.IsNullOrEmpty(ControlName) || !string.IsNullOrEmpty(Tag);
}
}
The XAMLControlInfo
class holds a reference to the control itself (TheControl
), the name of the control (ControlName
), the value in the Tag
property, the type of control (ControlType
) and a Boolean flag to identify if the control has an IsReadOnly
property (HasIsReadOnlyProperty
).
There's also a method contained in this class called ConsiderForSecurity()
. This method returns a True value if either the ControlName
or the Tag
properties contain a value. For a control to be secured, one or the other of these properties must contain a value, otherwise there's nothing to match up with a value in the ControlsToSecure
collection.
Security View Model Base Class
Earlier you created the stub of the SecurityViewModelBase
class. It's now time to write the code for the various methods you stubbed out. Before you write these methods, look at Figure 4 for an overview of each of the methods called from SecureControls()
.
The SecureControls()
method is called from the code-behind of the WPF element/container you want to secure. or from a command in your view model if you're using commanding. The SecureControls()
method calls the LoadControlsToSecure()
and the LoadControlsInXAMLContainer()
methods to populate the two properties ControlsToSecure
and ControlsInContainer
, respectively. Override the LoadControlsToSecure()
method so you can decide from which location to load the controls to secure. For example, you may hard-code the controls, retrieve them from an XML file, or load them from a database table. The LoadControlsInXAMLContainer()
method loops through all controls within the XAML element you pass in and loads the ControlsInContainer
collection.
Once both collections have been loaded, loop through the ControlsToSecure
collection and attempt to locate the control in the ControlsInContainer
collection. If you find a match, check to see if the user is a part of one of the roles identified in the security control. If they aren't a part of the role, then the state of the WPF control is modified to read-only, disabled, or made invisible.
LoadControlsToSecure Method
The LoadControlsToSecure()
method builds the data shown in Table 2. This code is written in the EmployeeViewModel
class because it will change based on where you wish to store the security control information. For this first example, the data is going to be hard-coded in the LoadControlsToSecure()
method, as shown in Listing 7.
Listing 7: A hard-coded version of the LoadControlsToSecure() method
protected override void LoadControlsToSecure(string containerName)
{
base.LoadControlsToSecure(containerName);
ControlsToSecure = new List<SecurityControl>
{
new SecurityControl
{
ContainerName = "EmployeeControl",
ElementIdentifier = "NewButton",
Mode = "collapsed",
RolesAsString = "Users123,Supervisor"
},
new SecurityControl
{
ContainerName = "EmployeeControl",
ElementIdentifier = "EmployeeID",
Mode = "readonly",
RolesAsString = "Admin,Supervisor"
},
new SecurityControl
{
ContainerName = "EmployeeControl",
ElementIdentifier = "Salary",
Mode = "hidden",
RolesAsString = "Admin"
},
new SecurityControl
{
ContainerName = "EmployeeControl",
ElementIdentifier = "SSN",
Mode = "disabled",
RolesAsString = "Supervisor"
},
new SecurityControl
{
ContainerName = "EmployeeControl",
ElementIdentifier = "SaveButton",
Mode = "disabled",
RolesAsString = "Admin,Supervisor"
}
};
}
LoadControlsInXAMLContainer Method
In Listing 4 you passed in a reference to the current window to the SecureControls()
method as shown as the first parameter in the code snippet below.
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// Set your Security Principal
SetSecurityPrincipal();
// Secure controls on this WPF window
_viewModel.SecureControls(this, "EmployeeControl");
}
This reference is passed into the LoadControlsInXAMLContainer()
method, shown in Listing 8 as the parameter element
. If the reference is a DependencyObject, then you know that you have a control that you can possibly secure. Build a new instance of a XAMLControlInfo
class and fill in the TheControl
and ControlType
properties. Attempt to cast the control as a FrameworkElement object. If the cast succeeds, extract the Name
and Tag
properties. If either of these properties are filled in, the ConsiderForSecurity()
method will return a True value.
Listing 8: The LoadControlsInXAMLContainer() method
protected virtual void LoadControlsInXAMLContainer(object element)
{
XAMLControlInfo ctl;
FrameworkElement fe;
if (element is DependencyObject dep)
{
ctl = new XAMLControlInfo
{
TheControl = element,
ControlType = element.GetType().Name
};
// Cast to 'FrameworkElement' so we can get the Name and Tag properties
fe = element as FrameworkElement;
if (fe != null)
{
ctl.ControlName = fe.Name;
if (fe.Tag != null)
{
ctl.Tag = fe.Tag.ToString();
}
}
if (ctl.ConsiderForSecurity())
{
// Is there a ReadOnly property?
ctl.HasIsReadOnlyProperty = element.GetType().GetProperty("IsReadOnly") == null ? false : true;
// Make sure there is not a null in ControlName or Tag
ctl.ControlName = ctl.ControlName ?? string.Empty;
ctl.Tag = ctl.Tag ?? string.Empty;
// Add control to be considered for security
ControlsInContainer.Add(ctl);
}
// Look for Child objects
foreach (object child in LogicalTreeHelper.GetChildren(dep))
{
// Make recursive call
LoadControlsInXAMLContainer(child);
}
}
}
If this control is to be added to the collection, you first determine if the control has an IsReadOnly
property. Next, you make sure that the Name
and Tag
properties don't contain a null, that instead they have an empty string. This just avoids a little extra checking later in your code. This control is then added to the ControlsInContainer
property. Any controls without a Name
or Tag
won't be added to the collection because there's no way to match it to one of the controls to secure.
Each control can contain other controls, so loop through any child controls and make a recursive call back to the LoadControlsInXAMLContainer()
method. In this way, you walk through the entire control tree within the reference to the control you passed to the SecureControls()
method.
SecureControls Method
The SecureControls()
method, Listing 9, is called from the code-behind of your WPF Window or User Control (or can be hooked to a command). After calling LoadControlsToSecure()
and LoadControlsInXAMLContainer()
, the code now loops through the ControlsToSecure
collection and for each control, it searches for the ElementIdentifier
as either a ControlName
or Tag
within the ControlsInContainer
. If the control is found, it loops through all roles to see if the user is in one of them. If one of the roles is found, that control is skipped. If none of the roles are found, the control is made invisible, read-only, or disabled, based on the Mode
property.
Listing 9: The SecureControls() method
public virtual void SecureControls(object element, string containerName)
{
XAMLControlInfo ctl = null;
CurrentPrincipal = Thread.CurrentPrincipal;
// Get Controls to Secure from Data Store
LoadControlsToSecure(containerName);
// Build List of Controls to be Secured
LoadControlsInXAMLContainer(element);
// Loop through controls
foreach (SecurityControl secCtl in ControlsToSecure)
{
secCtl.ElementIdentifier = secCtl.ElementIdentifier.ToLower();
// Search for Name property
ctl = ControlsInContainer.Find(c => c.ControlName.ToLower() == secCtl.ElementIdentifier);
if (ctl == null)
{
// Search for Tag property
ctl = ControlsInContainer.Find(c => c.Tag.ToLower() == secCtl.ElementIdentifier);
}
if (ctl != null && !string.IsNullOrWhiteSpace(secCtl.Mode))
{
// Loop through roles and see if user is NOT in one of the roles
// If not, change the state of the control
foreach (string role in secCtl.Roles)
{
if (CurrentPrincipal.IsInRole(role))
{
// They are in a role, so break out of loop
break;
}
else
{
// They are NOT in a role so change the control state
ChangeState(ctl, secCtl.Mode);
// Break out of loop because we have already modified the control state
break;
}
}
}
}
}
ChangeState Method
If the user isn't in one of the roles for the current control, pass in the reference to the XAMLControlInfo
object and the value in the Mode
property to the ChangeState()
method shown in Listing 10. Extract the reference to the control from TheControl
property. The switch statement decides what to do to the control to secure based on the mode parameter passed in. If the value is “disabled”, for example, then the visibility of the control is turned back on and the IsEnabled
property is set to False
. If the value is “readonly”, and the control has an IsReadOnly
property, that property is set to True, otherwise the IsEnabled property is set to True.
Listing 10: The ChangeState() method
protected virtual void ChangeState(XAMLControlInfo control, string mode)
{
Control ctl = (Control)control.TheControl;
switch (mode.ToLower())
{
case "disabled":
ctl.Visibility = Visibility.Visible;
ctl.IsEnabled = false;
break;
case "readonly":
case "read only":
case "read-only":
ctl.Visibility = Visibility.Visible;
ctl.IsEnabled = true;
if (control.HasIsReadOnlyProperty)
{
// Turn on IsReadOnly property
ctl.GetType().GetProperty("IsReadOnly").SetValue(control.TheControl, true, null);
}
else
{
ctl.IsEnabled = false;
}
break;
case "collapsed":
case "collapse":
ctl.Visibility = Visibility.Collapsed;
break;
case "hidden":
case "invisible":
ctl.Visibility = Visibility.Hidden;
break;
}
}
Try It Out
Run the application and you should see a screen that looks like Figure 5.
In the LoadControlsToSecure()
method in Listing 7, you have a value of “Users123”. Replace that with “Users”. If you're a member of the “Users” group, the New button shows up when you rerun the WPF application.
Secure Controls Using Database Table
Instead of hard coding the controls to secure as you did in Listing 7, create a database table where you can store the controls to secure in your WPF application.
SecurityControl Table
Below is the T-SQL script to create a SecurityControl
table in SQL Server. Add a SecurityControlId
column that can be a primary key value. Make this column an integer data type that increments automatically using the IDENTITY property. Open an instance of SQL Server and run this script to create this table.
CREATE TABLE SecurityControl (
SecurityControlId int IDENTITY(1,1) NOT NULL PRIMARY KEY NONCLUSTERED,
ContainerName nvarchar(100) NOT NULL,
ElementIdentifier nvarchar(100) NOT NULL,
Mode nvarchar(20) NOT NULL,
RolesAsString nvarchar(1024) NOT NULL
)
After creating the SecurityControl
table, enter the data shown in Figure 6 into this table.
WpfSecurityDbContext Class
Add the Entity Framework to your project using the NuGet package manager. Open the App.config
file and add a connection string, as shown in the following code snippet. Modify the Server
and Database
attributes in the connection string as appropriate for your environment.
<connectionStrings>
<add name="Sandbox"
connectionString="Server=Localhost; Database=Sandbox; Trusted_Connection=Yes;"
providerName="System.Data.SqlClient" />
</connectionStrings>
Add a new class to your project named WpfSecurityDbContextm
, as shown in Listing 11. Modify the constructor to use the name of the connection string you added in the App.config
file.
Listing 11: The WpfSecurityDbContext class used by EF to get security data from a database table
public class WpfSecurityDbContext : DbContext
{
public WpfSecurityDbContext() : base("name=Sandbox") { }
public virtual DbSet<SecurityControl> ControlsToSecure { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// Do NOT let EF create migrations or check database for model consistency
Database.SetInitializer<WpfSecurityDbContext>(null);
base.OnModelCreating(modelBuilder);
}
}
Modify SecurityControl Class
When you use the Entity Framework (EF), you need to decorate the class that maps to your table with a [Table] attribute. Open the SecurityControl.cs
file and add a using statement to reference the namespace where this attribute is found.
using System.ComponentModel.DataAnnotations.Schema;
Add the [Table] attribute just about the class definition as shown in the code below. You also need to add a new property, SecurityControlId
, to map to the primary key you added in the SecurityControl
table.
[Table("SecurityControl")]
public class SecurityControl
{
public int SecurityControlId { get; set; }
// REST OF THE PROPERTIES ARE HERE
}
SecurityControlManager Class
Instead of writing EF code to access the SecurityControl
table in your view model class, add a new class named SecurityControlManager
to your project. In this class, create a method named GetSecurityControls()
, shown in Listing 12, to which you pass in the container name. The container name is used if you have two or more Window or User Control objects in your WPF application that you wish to secure. Add a unique container name to each control in your SecurityControl
table, as shown in Figure 6.
Listing 12: The SecurityControlManager class gets the controls to secure from a table
public class SecurityControlManager
{
public List<SecurityControl> GetSecurityControls(string containerName)
{
List<SecurityControl> ret = new List<SecurityControl>();
using (WpfSecurityDbContext db = new WpfSecurityDbContext())
{
ret.AddRange(db.ControlsToSecure.Where(s => s.ContainerName == containerName).ToList());
}
return ret;
}
}
LoadControlsToSecure Method
Open the EmployeeViewModel
class and modify the LoadControlsToSecure()
method to create an instance of the SecurityControlManager
class. Call the GetSecurityControls()
method passing in the container name passed in to the SecureControls()
method from the Window_Loaded()
event procedure in the main window.
protected override void LoadControlsToSecure(string containerName)
{
SecurityControlManager mgr = new SecurityControlManager();
ControlsToSecure = mgr.GetSecurityControls(containerName);
}
Try It Out
Press F5 to run the WPF application and you should see the same controls turned off as before, when they were hard-coded.
Check for Binding Path
Normally, you don't have to name controls in WPF because you don't reference them from code-behind. The TextBox controls you want to affect with security have the Text
property with a {Binding} expression in them. You can query the binding class attached to a control and retrieve the value of the Path
property. This property can be used as the value in the ElementIdentifier
property in your SecurityControl
class. Consider the following TextBox control with a binding expression.
<TextBox Grid.Row="5" Grid.Column="1" Tag="SSN" Text="{Binding Path=SSN}" />
Both the Tag
and Text
properties have the value “SSN” that you can use to secure this control. Other controls in the EmployeeControl.xaml
file also have a binding and either a Name
or Tag
property set. Write a method to retrieve the Path
value so you can remove Name
and Tag
properties from the XAML where appropriate. Open the EmployeeControl.xaml
file and remove the Name
property from the “EmployeeID” TextBox control. Remove the Tag
property from the “Salary” TextBox control. And remove the Tag
property from the “SSN” TextBox control.
Add GetBindingName Method
Open the SecurityViewModelBase.cs
file and add a new method to retrieve the value of the Path
property from the Binding expression used on controls. The method named GetBindingName()
shown in Listing 13 uses a switch statement to determine the type of control is passed to this method. For a TextBox, you want to get the binding expression from the Text
property, so in the case statement for a text box you set the variable prop
to the Dependency property TextBox.TextProperty. Each control uses a different Dependency property based on which property you bind to most often.
Listing 13: The GetBindingName() method
protected virtual string GetBindingName(Control ctl)
{
string ret = string.Empty;
BindingExpression exp;
DependencyProperty prop;
if (ctl != null)
{
// Get the unique Dependency Property for each control that can be used to get a {Binding Path=xxx} expression
switch (ctl.GetType().Name.ToLower())
{
case "textbox":
prop = TextBox.TextProperty;
break;
case "label":
prop = Label.ContentProperty;
break;
case "menuitem":
prop = MenuItem.HeaderProperty;
break;
... // MORE CONTROLS GO HERE
default:
// Add your own custom/third-party controls
prop = GetBindingNameCustom(ctl);
break;
}
if (prop != null)
{
// Get Binding Expression
exp = ctl.GetBindingExpression(prop);
// If we have a valid binding attempt to get the binding path
if (exp != null && exp.ParentBinding != null && exp.ParentBinding.Path != null)
{
if (!string.IsNullOrEmpty(exp.ParentBinding.Path.Path))
{
ret = exp.ParentBinding.Path.Path;
}
}
}
}
if (!string.IsNullOrEmpty(ret))
{
if (ret.Contains("."))
{
ret = ret.Substring(ret.LastIndexOf(".") + 1);
}
}
return ret;
}
Call the GetBindingExpression()
method on the control, passing in the Dependency property. If you get a value back from this method, you check to see if the ParentBinding.Path.Path
property has a value in it. If it does, that's the name of the Path
property you passed to the Binding expression.
When you're loading the controls from the XAML container, you set the BindingPath
property by calling the GetBindingName()
method. Add the code shown in the snippet below.
protected virtual void LoadControlsInXAMLContainer(object element)
{
// REST OF THE CODE HERE
// See if there are any data bindings
ctl.BindingPath = GetBindingName(element as Control);
// REST OF THE CODE HERE
}
GetBindingNameCustom() Method
If you're using any third-party controls, you need a method where you can enter those. In the “default” for the switch statement, call a GetBindingNameCustom()
method shown below. In this method is where you can add your own custom controls and grab the appropriate Dependency property for that control.
protected virtual DependencyProperty GetBindingNameCustom(Control control)
{
DependencyProperty prop = null;
// Add custom control binding expressions here
switch (control.GetType().Name.ToLower())
{
case "my_custom_control":
// prop = ControlType.BindingProperty;
break;
}
return prop;
}
Modify the SecureControls Method
Because the BindingPath
property can now be filled into the XAMLControlInfo
class, you need to check that property to see if it matches the ElementIdentifier
property in the ControlsToSecure
collection. Open the SecurityViewModelBase
class and locate the SecureControls()
method. Within the loop where you search for a control by the name or tag, insert the code shown in bold in Listing 14.
Listing 14: Add code to look for a binding path that matches a control to secure
public virtual void SecureControls(object element, string containerName)
{
// REST OF THE CODE HERE
// Loop through controls
foreach (SecurityControl secCtl in ControlsToSecure)
{
secCtl.ElementIdentifier = secCtl.ElementIdentifier.ToLower();
// Search for Name property
ctl = ControlsInContainer.Find(c => c.ControlName.ToLower() == secCtl.ElementIdentifier);
if (ctl == null)
{
// Search for BindingPath
ctl = ControlsInContainer.Find(c => c.BindingPath.ToLower() == secCtl.ElementIdentifier);
}
// REST OF THE CODE HERE
}
}
Modify ConsiderForSecurity() Method
One last change to the code you need to make to support binding expressions is in the XAMLControlInfo
class. Open the XAMLControlInfo.cs file and locate the ConsiderForSecurity()
method. Add the line shown in bold to check if the BindingPath
property has a value or not.
public bool ConsiderForSecurity()
{
return !string.IsNullOrEmpty(BindingPath) ||
!string.IsNullOrEmpty(ControlName) ||
!string.IsNullOrEmpty(Tag);
}
Try It Out
Run the WPF application and if you've done everything correctly, you should see the same controls turned off just as they were before, even though you removed the Name
and Tag
properties.
Summary
In this article, you put together a data-driven security system for WPF. Using a data-driven approach allows you to change which controls to secure, generally without having to change any code. The code in this article does use reflection and thus can run a little slower; however, if you're loading a screen with data, most users won't even notice the extra half-second it might take to load the security information. You can also add some caching to this code to ensure that you only retrieve the security data one time. I've used code like this for years in my WPF applications and it's always worked well. I hope you can employ this code in your WPF applications when you need a security system.
Table 1: The list of properties/methods in the security view model base class
Property/Method | Description |
ControlsToSecure | A list of controls you wish to secure within a WPF container |
ControlsInContainer | The list of controls within a WPF container |
CurrentPrincipal | The current IPrincipal object for the logged-in user |
SecureControls() | Call this method to secure all controls within a WPF container. |
LoadControlsToSecure() | This method loads the list of controls to secure. |
LoadControlsInXAMLContainer() | This method loads all controls within a WPF container. |
Table 2: Set of sample security data stored in SecurityControl objects
ContainerName | ElementIdentifier | Mode | Roles |
EmployeeControl | NewButton | Collapsed | Users,Supervisor |
EmployeeControl | EmployeeID | ReadOnly | Admin,Supervisor |
EmployeeControl | Salary | Hidden | Admin |
EmployeeControl | SSN | Disabled | Supervisor |
EmployeeControl | SaveButton | Disabled | Admin,Supervisor |