Download VAL.zip - 1.44 MB

Introduction

The Visual Application Launcher (VAL) is a winforms application that allows delivery of icons to users that can be double clicked to launch the 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 formats 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 users '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 took it upon myself 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 was an excellent opportunity to refactor an old codebase using all the latest goodies and put together an example application that demonstrates them in use. VAL is meant as a complete example of a number of .Net technologies and design patterns

The solution contains examples of the following...

  • Using the Entity Framework (EF) to access data in an SQL Server Database
  • Using the Repository pattern to access data
  • Using a Unit of Work Factory and a Unit of Work to provide access to data across multiple repositories
  • Injecting a provider into the EF to allow caching of SQL queries
  • 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 the 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
  • Using attributes and reflection to identify object key fields for use with repository updates
  • Tests
  • Using Fake objects to Test the domain services

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 ProjectReferences directory of the solution. You can use the compiled assemblies, replace them with your own versions or download source \ binaries from associated web sites. If you don't want to use the compiled assemblies included in this download, just drop the replacements into the ProjectReferences 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 it's 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 screen that the majority of users of the system will 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 Caching Framework

When designing the data access, I was aware that there would be large amounts of data that would remain unchanged for periods of time and were good candidates for caching. I didn't want to have to roll my own caching mechanism, I wanted the same entity query syntax to determine whether to go to the cache or the database. e.g. This example code returns an array of File objects from the repository. On the first call, it should go to the database. On subsequent calls it should retrieve from the cache.

    	
public File[] GetGroupFiles(int groupId)
{
	var files = Repository.Query().Where(
	f => f.Permissions.Any
		(p => p.GroupID == groupId && f.IsActive == true))
		.ToArray();
		
	return files;
}	

To provide this functionality, we would need to intercept the SQL commands being executed against the Context and provide the caching functionality without requiring our consumers of the entity framework to change any of their code.

Luckily, there's a sample on MSDN that does exactly this. Check out the article and related information that describes the process and provides access to the source.

VAL has been configured to use the caching provider. It will cache information as SQL requests are made and will automatically invalidate the cache when updates are made. The only required changes were a few references, a few web.config entries and an extended 'Context' class. Very easy to implement in a simple application such as this.

The Entity Framework and the Repository Pattern

Access to data has been controlled using the Repository Pattern Another article that inspired me in this development was IRepository: one size does not fit all. This introduces the idea of Repository Traits that allow for different levels of access depending on your repository requirements. Each repository interface in VAL has been designed using the traits that provide the relevant level of access.

The class EntityRepository is the generic class used for repository access. We then create concrete classes for each type of repository that also enforce the repository access traits. In the following example, we have defined an interface named IUserActivityRepository that only allows adding data to the repository. The concrete class UserActivityRepository provides strongly typed access to the repository.

    	
public interface IUserActivityRepository :
	ICanAdd<UserActivity>
{
}
		
public class UserActivityRepository : 
	EntityRepository<UserActivity>, 
	IUserActivityRepository
{
	public UserActivityRepository() { }

}

The repository exposes an implementation of IQueryable which provides access to the underlying data using standard LINQ syntax

    	
var query = Repository.Query().Where(
	g => g.GroupUsers.Any
	(gu => gu.UserID == userId && g.IsGroupActive == true));

For reference purposes, I've also included an implementation of the Specification Pattern on the repository and have used the pattern in a couple of places. The pattern allows us to write code along the lines of...

    	
public File[] GetActiveFiles()
{
	var files = Repository.Find(new ActiveFilesSpecification());
	return files;
}

However, with the use of LINQ-to-SQL centralised in our domain services, the benefits of using the Specification aren't really realised and we can achieve the desired results without using the pattern.

A thread on Stack Overflow has a couple of good answers that go into this a bit further.

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 an object that implements IGroupRepository, which will allow us to test this service with Fake data at a later point

  
public class GroupDomainService : DomainServiceBase, IGroupService
{
	public GroupDomainService(IGroupRepository repository)
	{
		this.Repository = repository;
	}
	
	private IGroupRepository Repository { 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
  • 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, Resources.InvalidUserIdSpecified);
	#endregion

	var query = Repository.Query().Where(
		g => g.GroupUsers.Any
		(gu => gu.UserID == userId && g.IsGroupActive == true));

	var groups = query.ToArray();
	
	#region Variable dump
	if (log.IsInfoEnabled)
	{
		log.Info(string.Format("Got {0} group(s) from the repository", groups.Length));
	}
	#endregion	

	#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 of the identity of the caller
  • Starting a unit of Work
  • Accessing the particular domain service
  • Committing changes against the unit of work (if applicable)
  • Ensuring the context is destroyed
  • Returning data (if applicable)
  • Catching expected exceptions and converting into faults for the client
A method in the WCF services layer that will handle Exceptions and faults will read as follows

 
public User SaveUser(User user)
{
    #region Permissions
    RequireAdministrator();
    #endregion
            
	using (var uow = UnitOfWork.Start(Factory))
	{
        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));
		}
		
		UnitOfWork.Finish();                
		return user;
	}
	
}

All WCF services will have a class signature that reads similar to the following

 
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
public class UserService : WcfServiceBase, IUserService
{
	#region IUserService Members

	#region Ctor

	public UserService(IUserService service, IUnitOfWorkFactory factory) : base(factory)
	{
		this.Service = service;
	}

	#endregion
}

Some important points to note about the signature

  • The service is marked with AspNetCompatibilityRequirementsMode.Required, this is so we can use a Global.asax file to setup the Caching Provider
  • It inherits from WcfServiceBase, which takes an IUnitOfWorkFactory as a construct and exposes the factory as a protected property
  • 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 automatically.

The good thing about WCF services is that they are highly customisable, 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 4 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.

Service Contracts

In VAL, we control the codebase for both the client and the server. In the examples, 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 IFileService
{
	[OperationContract]
	File[] GetUserFiles(int userId);

	[OperationContract()]
	File[] GetAllFiles();
	
	// additional methods...
}	

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 WCF.ServiceInvoker to locate the contract endpoints as configured in the client app.config

Handling Security in the WCF service

As an application, 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, because we're using ChannelFactory our rather than the 'client proxies' generated by a tool like svcutil, we need to initialise the security credentials within the Factory creations

This happens within the ChannelFactoryManager class in the WCF.ServiceInvoker project. When we initially create the ChannelFactory, we setup the TokenImpersonationLevel and grab the DefaultNetworkCredentials.

private ChannelFactory CreateFactoryInstance<T>(string endpointConfigurationName, string endpointAddress)
{
    ChannelFactory factory = null;
    if (!string.IsNullOrEmpty(endpointAddress))
    {
        factory = new ChannelFactory<T>(endpointConfigurationName, new EndpointAddress(endpointAddress));
    }
    else
    {
        factory = new ChannelFactory<T>(endpointConfigurationName);
    }

    factory.Credentials.Windows.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation;
    factory.Credentials.Windows.ClientCredential = System.Net.CredentialCache.DefaultNetworkCredentials;
    
    factory.Faulted += FactoryFaulted;
    factory.Open();

    return factory;
}

 	
<security mode="TransportCredentialOnly">
    <transport clientCredentialType="Windows" proxyCredentialType="None" realm="" />
    <message clientCredentialType="UserName" algorithmSuite="Default" />
</security>

In the WcfServiceBase class, there are a couple of protected methods that will help us enforce security. Because security is a configuration value, we have to use imperative rather than declarative to ensure the identity of the caller

Calling method RequireAdministrator will ensure that the caller is a member of the domain group configured as AdministratorGroup in the .config file of the WCF service. You can also call RequireMembershipOf with any ad-hoc group name. RequireMembershipOf demonstrates how to retrieve the identity of the caller by querying OperationContext and ServiceSecurityContext to retrieve PrimaryIdentity

 
protected void RequireMembershipOf(string group)
{
    OperationContext oc = OperationContext.Current;
    ServiceSecurityContext ssc = oc.ServiceSecurityContext;
    string client = ssc.PrimaryIdentity.Name;
    string membershipClause = string.Format("Testing user '{0}' membership of group '{1}'", client, group);

    try
    {
        var permission = new PrincipalPermission(null, group, true);
        permission.Demand();
    }
    catch (System.Security.SecurityException ex)
    {
        var fault = new SecurityFault(group);
        throw new FaultException<SecurityFault>(fault, new FaultReason(ex.Message + Environment.NewLine + membershipClause));
    }
}

protected void RequireAdministrator()
{
    string adminGroup = ConfigurationManager.AppSettings["AdministratorGroup"];
    RequireMembershipOf(adminGroup);
}

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 instance 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 standard T4 template to generate POCO objects. Once the objects were created, a number of amendments have been made to them.

  • All domain model objects inherit from PocoEntityBase
  • All properties have been explicitly marked with the DataMember attribute
  • The property that identifies the object has been marked with a custom EntityKey attribute

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 a couple of properties, 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 seperate 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 it's 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 on the domain model. any consumers of our Model will automatically have validation available to them with very little effort. Our winforms application is enforcing 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 Invoker

Using svcutil you can create client side proxy classes to invoke your service.

I found some code on Stack Overflow that allows you to invoke methods against the interface by using the ChannelFactory. The code takes care of a few things, such as looking up the configuration of your services and caching the channels for reuse. This lets us invoke a service from client code using the following syntax

 	
var user = Session.WCF.InvokeService<IUserService, User>(
	proxy => proxy.GetUser(Environment.UserName)
);

This reduces some code duplication that can occur when creating the client side access. It also means that all comments that were placed against our Interface methods show up in Intellisense - a small detail but a useful one.

Unit Tests

The last project in the VAL solution contains a number of unit tests. Rather than using a Mocking framework, I've included 'Fake' objects such as FakeUnitOfWork, FakeUnitOfWorkFactory and various Fake Repositories. This lets us create some dummy hard coded data within the fake repositories which we can query and return results from using our existing domain services.

Within the unit test initialisation code, we setup StructureMap to use our fake objects

 	
ObjectFactory.Initialize(
    x =>
    {
        x.AddRegistry<TestRegistry>();
        x.For<IGroupService>().Use<GroupDomainService>();
    }
);

// Within the 'TestRegistry' setup...
this.For<IUnitOfWorkFactory>().Use<FakeUnitOfWorkFactory>();
this.For<IUnitOfWork>().Use<FakeUnitOfWork>();
// etc etc...

// When we try to get an instance of IGroupService, StructureMap will now inject the dependencies into the domain services using the 
// fake objects we have defined           
this.Service = ObjectFactory.GetInstance<IGroupService>();                        

I'm using the standard inbuilt 'Visual Studio Unit Testing Framework' for the test setups

[TestMethod()]
[ExpectedException(typeof(UserAlreadyExistsException))]
public void Same_Windows_Identity_Throws_Exception()
{
    // An object with WindowsIdentityName 'DMORLEY' already exists in our FakeUserRepository, we shouldn't be able to
    // add another one
    var someUser = new User
    {
        Forename = "Dylan",
        Surname = "Morley",
        WindowsIdentityName = ExistingUserName,
        Id = 0,
        IsActive = true
    };

    using (var uow = UnitOfWork.Start(Factory))
    {
        // Trying to call this method should throw exception
        someUser = Service.SaveUser(someUser);
        UnitOfWork.Finish();
    }
}

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.

While these tests are overly verbose and would require more maintenance than simply using mocking frameworks, hopefully they will help newcomers to testing step through and understand what is happening without hiding too much away in compiled Mocking framework.

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 & records.

Alarm bells should be ringing here. You have users all connected from machine SERVER01 all using the same credentials of Admin, sharing a 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 signon 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 signon 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 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 a 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 'Code Project', 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 underlying file type. As long as the account running 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 it's 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 examples 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 you towards resources or simply make projects open source.

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架
新浪微博粉丝精灵,刷粉丝、刷评论、刷转发、企业商家微博营销必备工具"