Introduction
The Visual Application Launcher (VAL) is a WinForms application that allows delivery of icons to users that can be double clicked to launch a file.
Background
VAL is useful for delivering bespoke applications, databases, and spreadsheets to users. In a corporate environment, it is very common to have data in formats
such as these that must be referred to by different departments.
E.g., Amy works in accounts, she needs access to the 'Budget Data' spreadsheet, an accounting Access database, an Intranet .NET web application, and an in-house
.NET WinForms application. John works in marketing, he needs access to the 'Budget Data' spreadsheet and the Intranet .NET web application. Dylan works in IT and needs
access to everything! How would you maintain all of this information? If the budget data spreadsheet is on a shared network drive, how will the user know where to go to launch the file?
This can be emailed around to the different users, but wouldn't it be easier if all of this could be centrally maintained and a list of 'shortcuts' delivered to the users
in a fancy user interface?
This is something that is often centrally managed for the main functionality of a user profile. Using the 'Amy' example, the domain administrator would grant permissions
on the account to deliver finance specific software that may be available to select from the user's 'Start' menu. However, this will rarely go to the granular level
of particular workbooks located on shares.
This is where VAL comes in - VAL has the concept of Groups, Users, and Files. Users and Files can be assigned to Groups and the system determines which Files the
User has access to by calculating their overall group membership.
Since the database schema for this application is simple, I decided to create an application that tests many of the current technologies and Design Patterns being
used in .NET today. While this works, it's massively over engineered for what it needs to achieve!
VAL is meant as a complete example of a number of .NET technologies and Design Patterns.
The solution contains examples of the following...
- Using Entity Framework 4.2 (EF) to access data in a SQL Server database
- Using the Repository pattern to access data
- Using Dependency Injection (DI) to create loosely coupled domain services
- Using StructureMap to inject dependencies
- Using WCF services to control access to domain services
- Using the
ChannelFactory
to invoke instances of WCF services
- Handling Exceptions in the WCF service
- Creating custom behaviours for WCF to control StructureMap and Errors
- Creating custom Faults, throwing Faults to our client application
- Creating a domain model using POCO objects
- Using Data Annotations to provide instance validation of our POCO objects
- Tests
- Using Moq with fake data to test method implementations
Prerequisites
You'll need at least the following to use the application...
- SQL Server 2005 or later
- A machine (local or remote) with IIS
- Visual Studio 2010
- .NET 4.0
VAL also uses a number of Open Source libraries which are included as compiled assemblies and are located in the packages directory of the solution.
You can use the compiled assemblies, replace them with your own versions, or download source \ binaries from the associated web sites. The packages directory
was created by NuGet, so you can simply update the packages if you are using NuGet.
If you don't want to use the compiled assemblies included in this download, just drop the replacements into packages/lib and the solution will pick them up when you compile:
Getting Started
The first thing you'll need to do is create the database for the application, the virtual directory in IIS for the WCF services, and make a few changes to the app configuration.
I've included a ReadMe.txt that describes each setting and its impact on the system; please make sure you read through and follow the guide when trying
to get the application ready to run. Spending a few minutes going through the config is important.
Application Screens and Overview
The main user interface is a simple window that displays icons, this is the only screen that the majority of users of the system will ever see.
There are also administration screens that allow you to configure the different properties of VAL. Only members of the 'Administration Group' will have access to these screens.
Admin MDI Window
The admin window allows you to launch the various maintenance windows by clicking on the icons in the Toolbar. Each screen maintains a particular group of data.
User Maintenance
The user maintenance screen is where you create user profiles for use with VAL. Either manually type or use the 'Active Directory Browser' section to pick a user from your AD.
File Maintenance
File maintenance is where you define icons to be delivered to end users. Configure the location of the programs to launch, their types, the icon to display, and various other options.
Group Maintenance
The group maintenance screen is where you create groups and where you assign users and files to particular groups. This determines the set of overall
permissions (and therefore the icons displayed to the user).
That's a quick overview of the different screens available in VAL. Please see the associated help file included with the application which provides full details of each maintenance screen.
Using the Code
There's a lot of code to describe, so consider the following logical diagram first. This describes the data flow process from the highest level (the UI) across
application boundaries (WCF communication) to the lowest level (the SQL database).
This article will describe the process from the lowest level first.
The Database
There's very little to describe in the database, there are only tables, indexes, and relationships to consider in the schema. There are no views or procedures
used by the application source code. Cascade operations will occur on related tables during a Delete operation.
The Entity Framework and the Repository Pattern
Access to data has been controlled using the Repository Pattern.
The class EntityRepository<t>
is the generic class used for repository access. The interface IRepository
is used in all service constructs,
so we can use Dependency Injection to provide different implementations at run time. This release (29/12/2012) has changed significantly from the previous version
which all used concrete repository classes and restricted access based on the requirements of individual repositories. This approach removes a large number
of classes from the solution, simplifies the overall project structure, and improves the ease of unit testing.
public interface IRepository<T>
{
}
public class EntityRepository&:
EntityRepository<T> where T : PocoEntityBase
{
}
The repository exposes an implementation of IQueryable<t>
which provides access to the underlying data using standard LINQ syntax.
var query = from g in Repository.Table
where g.GroupUsers.Any(gu => gu.UserID == userId && g.IsGroupActive == true)
select g;
The Domain Services
In VAL, the domain services are where most of the magic happens. The services here will perform a number of functions such as:
- Tracing and logging
- Parameter validation and exception raising
- Repository access to perform data retrieval \ updates
The domain services have been designed with Dependency Injection in mind. A typical class signature for a service will read as follows. The DomainServiceBase
class
gives access to some basic logging functionality to be shared by all service classes. The only construct on the service requires objects that implement IRepository
,
which will allow us to test this service with fake data at a later point.
public class GroupDomainService : DomainServiceBase, IGroupService
{
public GroupDomainService(IRepository<Group> repository,
IRepository<GroupUser> groupUserRepository,
IRepository<Permission> permissionsRepository,
IDbContext dataContext)
{
this.Repository = repository;
this.GroupUserRepository = groupUserRepository;
this.PermissionRepository = permissionsRepository;
this.DataContext = dataContext;
}
private IRepository<Group> Repository { get; set; }
private IRepository<GroupUser> GroupUserRepository { get; set; }
private IRepository<Permission> PermissionRepository { get; set; }
private IDbContext DataContext { get; set; }
}
A typical method in the domain services layer will do the following...
- Debug-Log entering the method
- Validate the required parameters
- Some sort of Repository access (Retrieve \ Update)
- Info-Log method values \ variables
- Debug-Log exiting the method
- Return data (if applicable)
public Group[] GetUserGroups(int userId)
{
#region Enter method Tracing
if (log.IsDebugEnabled)
{
log.Debug("Entered " + this.GetType().ToString() + " - " +
System.Reflection.MethodBase.GetCurrentMethod().ToString());
}
#endregion
#region Guard Parameter Validation
Guard.Against<ArgumentException>(userId <= 0, "User id must be greater than zero");
#endregion
var query = from g in Repository.Table
where g.GroupUsers.Any(gu => gu.UserID == userId && g.IsGroupActive == true)
select g;
var groups = query.ToArray();
#region Exit Method Tracing
if (log.IsDebugEnabled)
{
log.Debug("Completed " + this.GetType().ToString() + " - " +
System.Reflection.MethodBase.GetCurrentMethod().ToString());
}
#endregion
return groups;
}
The Application Services (WCF)
The WCF layer provides communication endpoints for the consumers to access the underlying subsystems. The WCF layer is responsible for the following operations:
- Security check the identity of the caller
- Access the particular domain service
- Return data (if applicable)
- Catch expected exceptions and convert into faults for the client
A method in the WCF services layer that will handle Exceptions and faults will read as follows:
[VALAdministrators(SecurityAction.Demand)]
public User SaveUser(User user)
{
try
{
user = Service.SaveUser(user);
}
catch (BusinessRuleException ex)
{
var fault = new BusinessRulesFault(ex);
throw new FaultException<BusinessRulesFault>(
fault, new FaultReason(fault.Message));
}
catch (UserAlreadyExistsException ex)
{
var fault = new UserFault(ex, user.WindowsIdentityName);
throw new FaultException<UserFault>(fault, new FaultReason(fault.Message));
}
return user;
}
All WCF services will have a class signature that reads similar to the following:
public class StandardUserService : WcfServiceBase, IUserService
{
#region IUserService Members
#region Ctor
public StandardUserService(IFileService fileService,
IUserService userService)
{
this.FileService = fileService;
this.UserService = userService;
}
#endregion
}
Some important points to note about the signature:
- It inherits from
WcfServiceBase
, a simple base class to provide logging functionality via the protected log4net.ILog
instance
- The WCF service construct requires arguments
The last point is important, WCF will normally take care of creating service objects and it expects a parameterless construct. If we want to use the services with
StructureMap for Dependency Injection, then we need to be able to pass parameters to the construct. We therefore need a way of allowing parameters and having them wire
up with StructureMap automagically.
The good thing about WCF services is that they are highly customizable, we can create Service Behaviours, and configure our WCF environment to use them.
Between Jimmy Bogard
and Scott Griffin, I borrowed the solution they found for the problem. :o)
The four classes in the ServiceBehaviour folder (StructureMapInstanceProvider
, StructureMapServiceBehavior
,
StructureMapServiceHostFactory
, StructureMapServiceHost
) provide the functionality for wiring up WCF services to StructureMap,
allowing us to specify the dependencies to be injected into the constructs.
Dependency Chain
There is a dependency chain in the system that is resolved by StructureMap. When an instance of an object is requested, if it has a parameterised construct, then StructureMap will also attempt
to inject an instance of *that* object into the instance.
In VAL, the dependency resolution begins when a request to a WCF service method is received. This initiates a dependency chain that looks similar to this graphic.
Once the DataSettings
class has been initialised, all dependencies are fulfilled and StructureMap is able to create instances of every object in the chain. It's important
to understand this concept, if there is an error in one of the classes deeper into the chain, it will cause an Exception in Structuremap.
Unit of Work Behaviour
Entity Framework 4.2 uses the new DbContext
API, which is a 'Unit of Work'. During its scope (which is per HTTP request) we can make numerous changes
to the entities which are change tracked by the context. Until we call SaveChanges
, none of these changes are actually committed to the database.
StructureMap will create this single instance per request and inject into any other object that requires an instance as part of its construct.
In order to aid testing and to ensure our services remain loosely coupled, we can abstract out the core functionality we require from the data context into a new interface,
IDbContext
.
public interface IDbContext
{
IDbSet<TEntity> Set<TEntity>() where TEntity : PocoEntityBase;
int SaveChanges();
EntityState GetState(object entity);
void SetModifed(object original, object updated);
void SetEntityState(object entity, EntityState state);
}
The method signatures for IDbContext
allow us a way to retrieve and update data. The concrete implementation of this interface is then handled
in the DataContext
(in project VAL.Data).
Service Behaviours
In order for StructureMap to create instances of the WCF services and resolve all dependencies, we need to attach the behaviour into the WCF service. We can apply this as part of the
service instantiation within our implementation of IServiceBehavior
. The class StructureMapServiceBehavior
implements this interface, and has a method body
for ApplyDispatchBehavior
.
public void ApplyDispatchBehavior(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase)
{
var errorHandler = new ErrorHandler();
foreach (ChannelDispatcherBase cdb in serviceHostBase.ChannelDispatchers)
{
ChannelDispatcher cd = cdb as ChannelDispatcher;
if (cd != null)
{
cd.ErrorHandlers.Add(errorHandler);
foreach (EndpointDispatcher ed in cd.Endpoints)
{
ed.DispatchRuntime.InstanceProvider =
new StructureMapInstanceProvider(serviceDescription.ServiceType);
}
}
}
}
In the above code, we create an instance of ErrorHandler
which provides our custom service behaviours. We attach the error handler
instance to the ChannelDispatcher
and add new instances of StructureMapInstanceProvider
to the InstanceProvider
, this allows StructureMap
to intercept the request for the service and inject the required dependencies. Now, every time StructureMap is asked to create an instance of a WCF service,
it will apply our custom behaviours.
Service Contracts
In VAL, we control the codebase for both the client and the server - I wanted to share the definitions of the services between both.
I therefore created an assembly named VAL.Contracts
that contains definitions of the service methods. A service interface will read similar to:
[ServiceContract()]
public interface IStandardUserService
{
[OperationContract]
File[] GetUserFiles(int userId);
}
The Interfaces provide the contracts between the client and the server that allow them to communicate. The WCF services implement these interfaces and the client
can invoke these implementations using the WCFServiceClient
code to locate the contract endpoints as configured in the client app.config.
Handling Security in the WCF Service
VAL is designed to be run in a secure 'trusted' environment such as a corporate network. Additionally, the information transferred by VAL cannot be classified as sensitive.
Therefore, we can choose a security model based on the following:
- System security can be roughly grouped into 'Administrator' and 'Non Administrator' functionality
- We don't need encrypted transport
- We need to enforce group membership of the user account running the VAL client
We could use WSHttpBinding
here, but for this model, BasicHttpBinding
is fine. Much like the classic 'Web Service' model, we just need to call
a method and make sure that the user is allowed access to that method.
We can accomplish this by setting up the configuration bindings to transfer the user account details. However, we need to initialise the security credentials within the client proxy base.
This happens within the WCFServiceClient
class in the VAL UI project. The client provides a custom implementation of ClientBase
which allows us to set the credentials
and also provide a Dispose pattern for handling clean up.
public class WCFServiceClient<T> : ClientBase<T>,
IDisposable where T : class
{
#region ctors
public WCFServiceClient()
{
this.ClientCredentials.Windows.AllowedImpersonationLevel =
System.Security.Principal.TokenImpersonationLevel.Impersonation;
this.ClientCredentials.Windows.ClientCredential =
System.Net.CredentialCache.DefaultNetworkCredentials;
}
#endregion ctors
void IDisposable.Dispose()
{
if (State == CommunicationState.Faulted)
{
Abort();
}
else
{
try
{
Close();
}
catch
{
Abort();
}
}
}
}
<security mode="TransportCredentialOnly">
<transport clientCredentialType="Windows" proxyCredentialType="None" realm="" />
<message clientCredentialType="UserName" algorithmSuite="Default" />
</security>
Custom Security Attributes
Because security is a configuration value, we can keep the code clean by defining our own implementation of CodeAccessSecurityAttribute
which allows us to read
a string from the config and apply that as the 'Require group membership' value.
public class VALAdministratorsAttribute : PrincipalPermissionExAttribute
{
public VALAdministratorsAttribute(SecurityAction action)
: base(action, "AdministratorGroup")
{
}
}
The VALAdministratorsAttribute
class constructs an instance of PrincipalPermissionExAttribute
and instructs it to read the value
of AdministratorGroup
from the config file. This value is then used to enforce the Security.Demand
call.
Any permissions that fail are reported back to the client application as a SecurityFault
.
Handling Faults in the WCF Service
There are a number of circumstances where the domain services will throw exceptions. One such example is object validation, which should occur on the client application
but will also happen on the server as part of 'Save' operations. If the object is invalid, an exception will be raised.
In cases where faults are expected, we need to return this information to the client so it can be displayed to the user. In all other cases, we want to log the exception
details on the server but shield the exception details from the user. Expected exceptions are caught by the WCF service and converted into Faults that are then returned
to the client. The client is responsible for catching and handling the expected Faults.
The Domain Model
The model was generated using the 'Reverse Engineer Code First' context menu that is available after installing
Entity Framework Power Tools. Once the entities
had been generated, a number of amendments were made.
- All domain model objects inherit from
PocoEntityBase
The PocoEntityBase
class provides some functionality for enforcing simple instance rules and taking advantage
of the DataAnnotations syntax that is used in MVC. The class
has anumber of properties, Id
, IsValid
and ErrorMessage
, that will allow you to check for broken rules and display the
appropriate error message to the caller.
[IgnoreDataMember()]
public bool IsValid
{
get
{
this.errors = DataValidator.Validate(this);
return (!errors.Any());
}
}
[IgnoreDataMember()]
public string ErrorMessage
{
get
{
var errorText = new StringBuilder();
foreach (var error in errors)
{
errorText.Append(error.ErrorMessage + Environment.NewLine);
}
return errorText.ToString();
}
}
To use this functionality, a Model class must have associated MetaData that provides the rules to be enforced. Due to the Model classes being generated by templates,
a bit of a hacky approach is used by creating Buddy Classes that provide only metadata in a separate
partial class. This allows you to auto generate your main model, while keeping your custom data annotations for model validation safe.
This MetaData is automatically wired into MVC, but in WinForms, we need to attempt to retrieve the MetaData ourselves. The DataValidator
class exposes
a single method that will attempt to validate an object by wiring up its MetaData and calling the Validator.TryValidateObject
method.
public class DataValidator
{
public static List<ValidationResult> Validate(object instance)
{
Type instanceType = instance.GetType();
Type metaData = null;
var metaAttr = (MetadataTypeAttribute[])
instanceType.GetCustomAttributes(typeof(MetadataTypeAttribute), true);
if (metaAttr.Count() > 0)
{
metaData = metaAttr[0].MetadataClassType;
}
else
{
throw new InvalidOperationException(
"Cannot validate object, no metadata " +
"assoicated with the specified type");
}
TypeDescriptor.AddProviderTransparent(
new AssociatedMetadataTypeTypeDescriptionProvider(
instanceType, metaData), instanceType);
var results = new List<ValidationResult>();
ValidationContext ctx = new ValidationContext(instance, null, null);
bool valid = Validator.TryValidateObject(instance, ctx, results, true);
return results;
}
}
You can now create MetaData classes for your model objects in the standard format.
[MetadataType(typeof(GroupMetaData))]
public partial class Group
{
public Group()
{
}
#region Internal MetaData class
internal class GroupMetaData
{
#region Primitive Properties
[Required(ErrorMessage = "You must enter a description for the group")]
[DisplayName("Description:")]
[StringLength(50)]
public virtual string Description {get; set;}
#endregion
}
#endregion
}
This keeps all of the 'simple' rules in one place in the domain model. Any consumers of our Model will have validation available to them with very little effort.
Our WinForms application enforces the rules in the following manner:
if (!this.group.IsValid)
{
Messaging.ShowError(group.ErrorMessage);
return;
}
The Model classes really are the key to the system. They provide access to the data in strongly typed form, they form the basis of the messages to be transferred
over the wire, and they provide information about themselves to the client for validation.
WCF Service Invocation
Using svcutil, you can create client side proxy classes to invoke your service.
We can then change the generated class to use our custom base class that provides the security initialisation and Dispose pattern. The standard using
pattern is
then adhered to when calling our service methods.
using (var service = new VAL.ClientServices.UserServiceClient())
{
files = service.GetUserFiles(Convert.ToInt32(e.Argument));
}
Unit Tests - Domain Classes
The last project in the VAL solution contains a number of unit tests. The tests make use of the Moq framework and some dummy data which we can query
and return results from using our existing domain services.
Within the unit test initialisation code, we create the Mock objects and Setup the repository methods to use our fake data:
[TestInitialize]
public void TestInitialize()
{
var list = TestDataObjectCreator.GetFilesList();
_fileRepository.Setup(f => f.Table).Returns(list.AsQueryable());
_fileRepository.Setup(f => f.GetById(It.IsAny<int>())).Returns
((int id) => list.Find(i => i.Id == id));
_fileRepository.Setup(f => f.Delete(It.IsAny<File>())).Callback
((File item) => list.Remove(item));
_fileRepository.Setup(f => f.Add(It.IsAny<File>())).Callback
((File item) =>
{
if (item.Id == 0)
{
list.Add(item);
}
});
_fileTypeRepository.Setup(f => f.Table).Returns(
TestDataObjectCreator.GetFileTypes().AsQueryable());
this.Service = new FileDomainService(this._fileRepository.Object,
this._fileTypeRepository.Object, _dataContext.Object);
}
I'm using the standard built-in 'Visual Studio Unit Testing Framework' for the test setups:
[TestMethod()]
[ExpectedException(typeof(UserAlreadyExistsException))]
public void Same_Windows_Identity_Throws_Exception()
{
var someUser = new User
{
Forename = "Dylan",
Surname = "Morley",
WindowsIdentityName = ExistingUserName,
Id = 0,
IsActive = true
};
someUser = Service.SaveUser(someUser);
}
In the above example test, we are ensuring that trying to save an object with the same WindowsIdentityName
as an existing object will throw an exception named
UserAlreadyExistsException
.
This is one of the major benefits of using a Dependency Injection framework such as StructureMap. The time we have spent creating loosely coupled domain
services using interfaces in our constructors is rewarded here, as we can send whatever test objects we like into the services and ensure the functionality within them works
as expected without requiring any underlying real data access.
Moq provides a simple and concise way to swap out the concrete implementations with Mock objects to ensure our service logic is correct and functioning as expected.
Unit Tests - WCF Services
A well as testing our domain classes, there are tests for the WCF service implementations. The WCF services have a different concern to the domain classes, so we only want
to test that they are doing their job as expected. We don't need to test 'over the wire' here, we can assume that this has all been tested by Microsoft! We just want to test
that our code is operating correctly.
In these tests, we can simply Mock the domain service return values - we don't need to test these again, that's all been done with our previous tests. We can follow
a strict Setup -> Act pattern for this stage of testing.
Consider the following test. All I want to test is that whenever an inactive user is requested, a FaultException
is returned by the WCF service. We can therefore
setup the domain service to simply return an Inactive user object we create in the test. All we are testing here is that the exception is raised and returned as expected.
[TestMethod]
[ExpectedException(typeof(FaultException<UserFault>))]
public void Get_Inactive_User_Throws_Fault()
{
string windowsName = "DMORLEY";
var user = new User
{
WindowsIdentityName = windowsName,
Id = 1,
Forename = "Dylan",
Surname = "Morley",
IsActive = false
};
_userService.Setup(us => us.GetUser(windowsName)).Returns(user);
var result = this.Service.GetUser(windowsName);
}
Beyond Code - Application Usage
Forgetting the code, what else is VAL useful for?
Access Databases - Prior to Access 2007
VAL will help you manage Access database usage, particularly if you have databases being used in a thin client (Citrix \ Terminal Services) environment.
In Access 2007, Microsoft changed the security model for the new database format to remove user level security. However, Access 2007 is backwards compatible
and can still open the .mdb format and work with user security. A number of organisations are still using older versions of Office or have invested time
in creating user-level security based databases, so this functionality may still be useful.
VAL allows you to specify a Network workgroup. This should be an mdw file that is stored somewhere on your local network that all users of VAL would have access to.
The network workgroup is the central location where you would manage user accounts and group membership.
Access is notorious for data corruption, especially so in a thin client environment. Imagine that several users are all connected to the same Citrix Server,
which we'll call SERVER01. They haven't signed on to a particular workgroup and are all using the default user account which is 'Admin'. This means that Access will
attempt to create record locks for both the default system.mdw workgroup in the 'Microsoft Office' installation directory on SERVER01 and also
for the particular Access database. These are LDB files, which contain a record of the user and machine name locking the file and records.
Alarm bells should be ringing here. You have users all connected from machine SERVER01 all using the same credentials of Admin, sharing an ldb file
on a disk where Microsoft Office is installed! This is a recipe for disaster and can lead to corruption of both the Access database and the system.mdw file.
There are a few rules to follow when using databases in this setup to avoid data corruption.
- Databases should be split into compiled front end (MDE) and data back end (MDB) files
- Users should launch their own copies of the front end
- Users should sign on through a workgroup
- Users should sign on to the workgroup using a user specific account, not using 'Admin'
- Users should sign on to their own copy of the workgroup
I've found that when using the above rules, data corruption of Access databases is at an absolute minimum.
VAL is geared towards helping out with this scenario; you can define applications as 'Access databases' and have VAL automatically distribute the compiled
front ends and workgroups to a user specific location. VAL will also attempt to sign the user on to the database transparently.
Workgroup Definition
For example, say you have your Network workgroup on a shared mapped drive, the Workgroup path may read something like K:\Public\AccessDatabases\Security\CompanyWorkgroup.mdw.
If you have defined your file in VAL as an Access database type and specified this workgroup, when you launch the Access database, VAL will take a copy
of the mdw file from the network location and put it in a location specific to the user. This could be in something like,
C:\Documents and Settings\username\Application Data\ or in any other location you like, as long as it is unique to the user.
VAL will then attempt to sign on via the copy of the workgroup which will now only ever contain one record lock for the individual user.
VAL will also pass the Environment.UserName
to the workgroup sign-on and attempt to sign the user into the workgroup. This ensures that the LDB
file for the Access database contains information such as SERVER01 DMORLEY, SERVER01 ANOTHERUSER, SERVER01 THIRDUSER.
By forcing the user to sign on using their account name, the shared LDB file for the Access database will contain unique user identifiers, even when using the same server thin client.
Access Permissions
Managing access permissions can be a tedious process. If you setup a user in VAL, ensuring that the user is also created in your Network workgroup and assigned group membership
adds another layer of complexity.
VAL allows you to clone a new user from an existing user. As well as creating all the icons for the user, it will attempt to copy Access database permissions from the source user,
ensuring that the user account and permissions are all correctly assigned.
This is achieved through ADODB and ADOX; please see the source file WorkgroupHelper
which uses Reflection to 'late-bind' the COM functionality. Use the 'New User'
wizard to step through the cloning process.
Access Databases - Summary
The workgroup settings for the Network workgroup should be defined in the app.config VAL settings for enterpriseWorkgroupDirectory
and enterpriseWorkgroupName
. Only define these if you are using legacy Access database security models, otherwise they can be left blank.
Internet links
VAL is also useful for distributing web site addresses as icons. For example, you may have a number of in-house intranet applications that can be accessed from particular URLs.
Individual users have to keep a record of these URLs in something like their browser favourites.
In VAL, you can create a new file and select the browser to launch, e.g., C:\Program Files\Internet Explorer\iexplore.exe. VAL will automatically retrieve the Internet Explorer
icon and assign it to your file. Now you can replace this with any image of your choice and then create an entry in the command line section of the file maintenance screen and point
it at any URL you like. Imagine we wanted an icon for the 'CodeProject', you would enter http://www.codeproject.com/.
When VAL attempts to launch this, it will build the following string to start
the application: "C:\Program Files\Internet Explorer\iexplore.exe" http://www.codeproject.com/.
This is a very simple way of delivering web applications to users as an Icon that represents your link. If any of the URLs change, you have a central location to maintain the data.
Distributing Files - General
You can always distribute files, regardless of the underlying file type. As long as the account running the VAL client has access to the location specific in the File Maintenance screen,
it can take a copy of the file and place in another location.
There are various reasons for wanting users to launch via a file copy, VAL lets you manage this easily.
Summary
This is a summary of the architecture of the Visual Application Launcher and its possible uses. As mentioned at the start of the article, it's an over-engineered solution
for such a simple application, but the real purpose here is to show how we can use various tools at our disposal.
Acknowledgements
Like most programmers, I learn a lot from Internet resources and example projects. A lot of the code in this solution can probably be found on the Internet in certain forms.
Where I've used complete classes, any headers and credits will point you to the original authors. In other cases, I've found snippets and solutions on message boards and incorporated
them into larger classes. In these cases, I can't credit everyone because I've forgotten where everything came from!
A big thank you to people who take time out of their busy schedules to answer questions, point us towards resources, or simply make projects Open Source.
To Do
The next step here is to create a WPF front-end rather than WinForms and have it consume the same service model. That'll be in a future article :)
History
- 29/12/2012 - Entity Framework update and code simplification.
- Updated Entity Framework to version 4.2 'Code First' syntax.
- Removed repository project \ Unit of Work implementation. Now using single generic repository and DbContext API.
- Removed all concrete repositories and repository traits. Much simpler and cleaner approach.
- Added NuGet packages.
- Added Moq framework and updated all tests to use Moq.
- 15/9/2011: Updated article to reflect changes made on 24/6/2011.
- Removed WCF Service Invoker and ChannelFactoryManager details, now obsolete.
- Added details about Unit of Work per Request pattern and how this is controlled from StructureMap.
- Added information about custom security attributes.
- 24/6/2011: Major changes to the overall structure of the project.
- Added an implementation of
IDispatchMessageInspector
to handle 'before' and 'after' WCF events.
- Unit of work now controlled through the
IDispatchMessageInspector
, a 'unit of work per request' approach. Simplifies code in the actual WCF service.
- Simplified WCF Services into two svc files, one 'User' service and one 'Administrator' service.
- Simplified the domain services and contracts, removed unnecessary classes.
- Added attribute based security to the Administrator Service.
- Removed WCF Service invoker proxy-generator. Access to WCF services now through
ClientBase<>
.
- Generally tidied up the organisation of the project and various other improvements :)
- 25/03/2011 - Edits. Grammar and readability.
- 24/03/2011 - Initial release.