可视化应用程序启动器
一个使用 WCF 服务、Entity Framework、仓储数据访问、仓储缓存、工作单元、依赖注入以及你能想到的所有其他流行词的 WinForms UI!
引言
可视化应用程序启动器 (VAL) 是一个 WinForms 应用程序,它允许向用户交付图标,用户可以双击图标来启动文件。
背景
VAL 对于向用户交付定制应用程序、数据库和电子表格非常有用。在企业环境中,以这些格式存在的数据必须由不同部门引用,这种情况非常普遍。
例如,Amy 在会计部门工作,她需要访问“预算数据”电子表格、一个会计 Access 数据库、一个内部 .NET Web 应用程序和一个内部 .NET WinForms 应用程序。John 在营销部门工作,他需要访问“预算数据”电子表格和内部 .NET Web 应用程序。Dylan 在 IT 部门工作,他需要访问所有内容!您将如何维护所有这些信息?如果预算数据电子表格位于共享网络驱动器上,用户如何知道去哪里启动文件?这可以通过电子邮件发送给不同的用户,但是如果所有这些都可以集中维护,并通过一个精美的用户界面向用户提供“快捷方式”列表,那不是更容易吗?
这通常是用户配置文件主要功能所集中管理的内容。使用“Amy”的例子,域管理员将授予帐户权限,以交付可能从用户“开始”菜单中选择的特定于财务的软件。然而,这很少会达到共享上特定工作簿的粒度级别。
这就是 VAL 的用武之地——VAL 具有组、用户和文件的概念。用户和文件可以分配给组,系统通过计算用户的整体组成员资格来确定用户有权访问哪些文件。
由于此应用程序的数据库模式很简单,我决定创建一个测试当前 .NET 中使用的许多技术和设计模式的应用程序。虽然这有效,但对于它需要实现的目标来说,它被过度设计了!
VAL 旨在作为许多 .NET 技术和设计模式的完整示例。
该解决方案包含以下示例...
- 使用 Entity Framework 4.2 (EF) 访问 SQL Server 数据库中的数据
- 使用仓储模式访问数据
- 使用依赖注入 (DI) 创建松散耦合的领域服务
- 使用 StructureMap 注入依赖项
- 使用 WCF 服务控制对领域服务的访问
- 使用
ChannelFactory
调用 WCF 服务实例 - 处理 WCF 服务中的异常
- 为 WCF 创建自定义行为以控制 StructureMap 和错误
- 创建自定义故障,将故障抛给客户端应用程序
- 使用 POCO 对象创建领域模型
- 使用数据注解为 POCO 对象提供实例验证
- 测试
- 使用 Moq 和虚假数据测试方法实现
必备组件
您至少需要以下内容才能使用该应用程序...
- SQL Server 2005 或更高版本
- 一台(本地或远程)安装了 IIS 的机器
- Visual Studio 2010
- .NET 4.0
VAL 还使用了许多开源库,这些库作为编译后的程序集包含在解决方案的 packages 目录中。您可以使用编译后的程序集,用您自己的版本替换它们,或从相关网站下载源代码/二进制文件。packages 目录由 NuGet 创建,因此如果您正在使用 NuGet,可以简单地更新包。
如果您不想使用此下载中包含的编译程序集,只需将替换文件放入 packages/lib,解决方案将在您编译时拾取它们
入门
您首先需要做的是为应用程序创建数据库,在 IIS 中为 WCF 服务创建虚拟目录,并对应用程序配置进行一些更改。
我提供了一个 ReadMe.txt,其中描述了每个设置及其对系统的影响;请确保在准备运行应用程序时仔细阅读并遵循指南。花几分钟查看配置非常重要。
应用程序屏幕和概述
主用户界面是一个显示图标的简单窗口,这是系统大多数用户唯一会看到的屏幕。
还有管理屏幕,允许您配置 VAL 的不同属性。只有“管理组”的成员才能访问这些屏幕。
管理员 MDI 窗口
管理窗口允许您通过单击工具栏中的图标来启动各种维护窗口。每个屏幕都维护一组特定的数据。
用户维护
用户维护屏幕是您为 VAL 创建用户配置文件的地方。您可以手动输入或使用“Active Directory 浏览器”部分从 AD 中选择一个用户。
文件维护
文件维护是您定义要交付给最终用户的图标的地方。配置要启动的程序的位置、它们的类型、要显示的图标以及各种其他选项。
组维护
组维护屏幕是您创建组以及将用户和文件分配给特定组的地方。这决定了整体权限集(以及因此显示给用户的图标)。
这是 VAL 中可用不同屏幕的快速概述。请参阅应用程序随附的相关帮助文件,其中提供了每个维护屏幕的完整详细信息。
Using the Code
有很多代码需要描述,所以请首先考虑以下逻辑图。这描述了从最高层(UI)跨应用程序边界(WCF 通信)到最低层(SQL 数据库)的数据流过程。
本文将首先描述从最低级别开始的过程。
数据库
数据库中几乎没有什么可描述的,模式中只有表、索引和关系需要考虑。应用程序源代码不使用视图或存储过程。在删除操作期间,级联操作将在相关表上发生。
Entity Framework 和仓储模式
数据访问已通过 仓储模式 进行控制。
类 EntityRepository<t>
是用于仓储访问的泛型类。接口 IRepository
用于所有服务构造中,因此我们可以使用依赖注入在运行时提供不同的实现。此版本(2012 年 12 月 29 日)与以前的版本相比发生了显著变化,以前的版本都使用具体的仓储类并根据各个仓储的需求限制访问。这种方法从解决方案中删除了大量的类,简化了整体项目结构,并提高了单元测试的便利性。
public interface IRepository<T>
{
// Method definitions
}
public class EntityRepository&:
EntityRepository<T> where T : PocoEntityBase
{
// Implementation of IRepository
}
仓储公开了 IQueryable<t>
的实现,该实现使用标准 LINQ 语法访问底层数据。
var query = from g in Repository.Table
where g.GroupUsers.Any(gu => gu.UserID == userId && g.IsGroupActive == true)
select g;
领域服务
在 VAL 中,领域服务是大部分“魔法”发生的地方。这里的服务将执行许多功能,例如:
- 跟踪和日志记录
- 参数验证和异常抛出
- 仓储访问以执行数据检索/更新
领域服务在设计时考虑了依赖注入。服务的典型类签名如下所示。DomainServiceBase
类提供了所有服务类共享的一些基本日志记录功能。服务上唯一的构造需要实现 IRepository
的对象,这将允许我们在稍后使用假数据测试此服务。
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; }
}
领域服务层中的典型方法将执行以下操作...
- 调试日志记录方法入口
- 验证所需的参数
- 某种形式的存储库访问(检索/更新)
- 信息日志记录方法值/变量
- 调试日志记录方法出口
- 返回数据(如果适用)
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;
}
应用程序服务 (WCF)
WCF 层为消费者提供了访问底层子系统的通信端点。WCF 层负责以下操作:
- 安全检查调用者身份
- 访问特定领域服务
- 返回数据(如果适用)
- 捕获预期异常并转换为客户端的故障
WCF 服务层中处理异常和故障的方法将如下所示
[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;
}
所有 WCF 服务都将有一个类似于以下的类签名
public class StandardUserService : WcfServiceBase, IUserService
{
#region IUserService Members
#region Ctor
public StandardUserService(IFileService fileService,
IUserService userService)
{
this.FileService = fileService;
this.UserService = userService;
}
#endregion
}
关于签名需要注意的一些重要点
- 它继承自
WcfServiceBase
,这是一个简单的基类,通过受保护的log4net.ILog
实例提供日志记录功能 - WCF 服务构造需要参数
最后一点很重要,WCF 通常负责创建服务对象,并且它期望一个无参数构造。如果我们要将服务与 StructureMap 用于依赖注入,那么我们需要能够将参数传递给构造。因此,我们需要一种方法来允许参数并让它们自动与 StructureMap 连接。
WCF 服务的好处是它们高度可定制,我们可以创建服务行为,并配置我们的 WCF 环境来使用它们。我从 Jimmy Bogard 和 Scott Griffin 那里借鉴了他们找到的解决方案。:o)
ServiceBehaviour 文件夹中的四个类(StructureMapInstanceProvider
、StructureMapServiceBehavior
、StructureMapServiceHostFactory
、StructureMapServiceHost
)提供了将 WCF 服务连接到 StructureMap 的功能,允许我们指定要注入构造函数的依赖项。
依赖链
系统中存在一个由 StructureMap 解析的依赖链。当请求一个对象实例时,如果它有一个带参数的构造函数,那么 StructureMap 也会尝试将 *该* 对象的实例注入到实例中。
在 VAL 中,当接收到对 WCF 服务方法的请求时,依赖解析开始。这会启动一个类似于此图的依赖链。
一旦 DataSettings
类被初始化,所有依赖项都已满足,并且 StructureMap 能够创建链中每个对象的实例。理解这个概念很重要,如果链中更深层次的某个类中存在错误,它将在 StructureMap 中引发异常。
工作单元行为
Entity Framework 4.2 使用新的 DbContext
API,它是一个“工作单元”。在其作用域内(每个 HTTP 请求),我们可以对由上下文跟踪更改的实体进行多次更改。在我们调用 SaveChanges
之前,这些更改都不会实际提交到数据库。StructureMap 将为每个请求创建这个单一实例,并将其注入到任何其他需要实例作为其构造一部分的对象中。
为了辅助测试并确保我们的服务保持松散耦合,我们可以将数据上下文所需的核心功能抽象到一个新接口 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);
}
IDbContext
的方法签名允许我们检索和更新数据。此接口的具体实现在 DataContext
(在 VAL.Data 项目中) 中处理。
服务行为
为了让 StructureMap 创建 WCF 服务的实例并解析所有依赖项,我们需要将行为附加到 WCF 服务中。我们可以在 IServiceBehavior
的实现中,作为服务实例化的一部分来应用此行为。StructureMapServiceBehavior
类实现了此接口,并具有 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);
}
}
}
}
在上述代码中,我们创建了一个 ErrorHandler
实例,它提供了我们的自定义服务行为。我们将错误处理程序实例附加到 ChannelDispatcher
,并将 StructureMapInstanceProvider
的新实例添加到 InstanceProvider
,这允许 StructureMap 拦截服务请求并注入所需的依赖项。现在,每当 StructureMap 被要求创建 WCF 服务的实例时,它将应用我们的自定义行为。
服务契约
在 VAL 中,我们控制客户端和服务器的代码库——我希望在两者之间共享服务定义。
因此,我创建了一个名为 VAL.Contracts
的程序集,其中包含服务方法的定义。服务接口将类似于:
[ServiceContract()]
public interface IStandardUserService
{
[OperationContract]
File[] GetUserFiles(int userId);
// additional methods...
}
接口提供了客户端和服务器之间的契约,允许它们进行通信。WCF 服务实现这些接口,客户端可以使用 WCFServiceClient
代码调用这些实现,以查找在客户端 app.config 中配置的契约端点。
处理 WCF 服务中的安全性
VAL 设计用于在安全的“受信任”环境(如公司网络)中运行。此外,VAL 传输的信息不能归类为敏感信息。因此,我们可以选择基于以下原则的安全模型:
- 系统安全大致可分为“管理员”和“非管理员”功能
- 我们不需要加密传输
- 我们需要强制运行 VAL 客户端的用户帐户的组成员身份
我们可以在这里使用 WSHttpBinding
,但对于此模型,BasicHttpBinding
就可以了。就像经典的“Web 服务”模型一样,我们只需要调用一个方法并确保用户被允许访问该方法。
我们可以通过设置配置绑定来传输用户账户详细信息。但是,我们需要在客户端代理基类中初始化安全凭据。
这发生在 VAL UI 项目中的 WCFServiceClient
类中。客户端提供了 ClientBase
的自定义实现,它允许我们设置凭据并提供用于清理的 Dispose 模式。
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>
自定义安全属性
因为安全性是一个配置值,我们可以通过定义我们自己的 CodeAccessSecurityAttribute
实现来保持代码整洁,这允许我们从配置中读取一个字符串并将其作为“Require group membership”值应用。
public class VALAdministratorsAttribute : PrincipalPermissionExAttribute
{
public VALAdministratorsAttribute(SecurityAction action)
: base(action, "AdministratorGroup")
{
}
}
VALAdministratorsAttribute
类构造 PrincipalPermissionExAttribute
的实例,并指示它从配置文件中读取 AdministratorGroup
的值。然后,此值用于强制执行 Security.Demand
调用。
任何失败的权限都将作为 SecurityFault
报告回客户端应用程序。
处理 WCF 服务中的故障
在某些情况下,领域服务会抛出异常。一个例子是对象验证,它应该发生在客户端应用程序上,但也会作为“保存”操作的一部分发生在服务器上。如果对象无效,将抛出异常。
在预期故障的情况下,我们需要将此信息返回给客户端,以便向用户显示。在所有其他情况下,我们希望在服务器上记录异常详细信息,但隐藏用户的异常详细信息。WCF 服务会捕获预期异常并将其转换为故障,然后返回给客户端。客户端负责捕获和处理预期故障。
领域模型
该模型是使用安装 Entity Framework Power Tools 后可用的“逆向工程 Code First”上下文菜单生成的。实体生成后,进行了一些修改。
- 所有领域模型对象都继承自
PocoEntityBase
PocoEntityBase
类提供了一些功能,用于强制执行简单的实例规则并利用 MVC 中使用的 DataAnnotations 语法。该类具有多个属性,Id
、IsValid
和 ErrorMessage
,这些属性允许您检查违反的规则并向调用者显示相应的错误消息。
[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();
}
}
要使用此功能,模型类必须具有关联的元数据,提供要强制执行的规则。由于模型类是由模板生成的,因此通过创建 伴生类 采用了一种有点“取巧”的方法,伴生类仅在单独的部分类中提供元数据。这允许您自动生成主模型,同时保持您的自定义数据注解以进行模型验证的安全。
此元数据会自动与 MVC 连接,但在 WinForms 中,我们需要尝试自行检索元数据。DataValidator
类公开了一个方法,该方法将通过连接其元数据并调用 Validator.TryValidateObject
方法来尝试验证对象。
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;
}
}
您现在可以以标准格式为模型对象创建元数据类。
[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
}
这将所有“简单”规则集中在领域模型中的一个位置。我们模型的所有消费者都可以轻松地进行验证。我们的 WinForms 应用程序以以下方式强制执行规则:
if (!this.group.IsValid)
{
Messaging.ShowError(group.ErrorMessage);
return;
}
模型类确实是系统的关键。它们以强类型形式提供数据访问,它们构成了通过网络传输消息的基础,并且它们向客户端提供有关自身的验证信息。
WCF 服务调用
使用 svcutil,您可以创建客户端代理类来调用您的服务。
然后我们可以更改生成的类以使用我们提供安全初始化和 Dispose 模式的自定义基类。然后,在调用我们的服务方法时,将遵循标准的 using
模式。
using (var service = new VAL.ClientServices.UserServiceClient())
{
files = service.GetUserFiles(Convert.ToInt32(e.Argument));
}
单元测试 - 领域类
VAL 解决方案中的最后一个项目包含许多单元测试。这些测试利用 Moq 框架和一些虚拟数据,我们可以使用我们现有的领域服务查询并返回结果。
在单元测试初始化代码中,我们创建 Mock 对象并设置仓储方法以使用我们的假数据
[TestInitialize]
public void TestInitialize()
{
var list = TestDataObjectCreator.GetFilesList();
// Setup the mock repository to use a list of objects as it's query source
_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));
// Setup the delete method to remove the item from the list we created above
_fileRepository.Setup(f => f.Delete(It.IsAny<File>())).Callback
((File item) => list.Remove(item));
// Setup the add method
_fileRepository.Setup(f => f.Add(It.IsAny<File>())).Callback
((File item) =>
{
if (item.Id == 0)
{
list.Add(item);
}
});
// We only need to setup the repository access for file types
_fileTypeRepository.Setup(f => f.Table).Returns(
TestDataObjectCreator.GetFileTypes().AsQueryable());
// Pass in the mock objects to our service ctor
this.Service = new FileDomainService(this._fileRepository.Object,
this._fileTypeRepository.Object, _dataContext.Object);
}
我正在使用标准的内置“Visual Studio 单元测试框架”进行测试设置
[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);
}
在上面的示例测试中,我们确保尝试保存一个与现有对象具有相同 WindowsIdentityName
的对象将抛出名为 UserAlreadyExistsException
的异常。
这是使用 StructureMap 等依赖注入框架的主要优点之一。我们花费在构造函数中使用接口创建松散耦合领域服务的时间在这里得到了回报,因为我们可以将我们喜欢的任何测试对象发送到服务中,并确保它们内部的功能按预期工作,而无需任何底层实际数据访问。
Moq 提供了一种简单而简洁的方式来将具体实现替换为 Mock 对象,以确保我们的服务逻辑正确且按预期运行。
单元测试 - WCF 服务
除了测试我们的领域类,还有 WCF 服务实现的测试。WCF 服务与领域类有不同的关注点,所以我们只希望测试它们是否按预期工作。我们不需要在这里测试“通过网络”,我们可以假设这已经由微软测试过了!我们只是想测试我们的代码是否正常运行。
在这些测试中,我们可以简单地模拟领域服务的返回值——我们不需要再次测试这些,这在之前的测试中都已经完成。我们可以为测试的这个阶段遵循严格的设置 -> 执行模式。
考虑以下测试。我只想测试,每当请求一个不活跃的用户时,WCF 服务会返回一个 FaultException
。因此,我们可以设置领域服务,使其简单地返回我们在测试中创建的一个不活跃用户对象。我们在这里测试的只是异常是否按预期抛出并返回。
[TestMethod]
[ExpectedException(typeof(FaultException<UserFault>))]
public void Get_Inactive_User_Throws_Fault()
{
// Setup
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);
// Act
var result = this.Service.GetUser(windowsName);
}
超越代码 - 应用程序使用
撇开代码不谈,VAL 还有什么用处?
Access 数据库 - Access 2007 之前
VAL 将帮助您管理 Access 数据库的使用,特别是如果您在瘦客户端 (Citrix \ 终端服务) 环境中使用了数据库。
在 Access 2007 中,微软改变了新数据库格式的安全模型,移除了用户级别安全。然而,Access 2007 向后兼容,仍然可以打开 .mdb 格式并使用用户安全。许多组织仍在D使用旧版本的 Office,或者投入时间创建基于用户级别安全的数据库,因此此功能可能仍然有用。
VAL 允许您指定一个网络工作组。这应该是一个 mdw 文件,存储在本地网络上的某个位置,所有 VAL 用户都可以访问。网络工作组是您管理用户帐户和组成员资格的中心位置。
Access 因数据损坏而臭名昭著,尤其是在瘦客户端环境中。想象一下,几个用户都连接到同一个 Citrix 服务器,我们称之为 SERVER01。他们没有登录到特定的工作组,并且都使用默认用户帐户“Admin”。这意味着 Access 将尝试为 SERVER01 上“Microsoft Office”安装目录中的默认 system.mdw 工作组以及特定的 Access 数据库创建记录锁定。这些是 LDB 文件,其中包含锁定文件和记录的用户和机器名称的记录。
警钟应该响起。您的用户都从机器 SERVER01 连接,都使用相同的管理员凭据,共享一个安装了 Microsoft Office 的磁盘上的 ldb 文件!这是一种灾难的预兆,可能导致 Access 数据库和 system.mdw 文件损坏。
在此设置中使用数据库时,应遵循一些规则以避免数据损坏。
- 数据库应分为编译后的前端 (MDE) 文件和数据后端 (MDB) 文件。
- 用户应启动自己的前端副本
- 用户应通过工作组登录
- 用户应使用特定于用户的账户登录到工作组,而不是使用“Admin”
- 用户应该登录到他们自己的工作组副本
我发现,当使用上述规则时,Access 数据库的数据损坏率达到了最低。
VAL 旨在帮助解决这种情况;您可以将应用程序定义为“Access 数据库”,并让 VAL 自动将编译后的前端和工作组分发到用户特定的位置。VAL 还将尝试透明地让用户登录到数据库。
工作组定义
例如,假设您的网络工作组位于共享映射驱动器上,工作组路径可能类似于 K:\Public\AccessDatabases\Security\CompanyWorkgroup.mdw。
如果您在 VAL 中将文件定义为 Access 数据库类型并指定了此工作组,那么当您启动 Access 数据库时,VAL 将从网络位置复制一份 mdw 文件并将其放入用户特定的位置。这可能在类似 C:\Documents and Settings\username\Application Data\ 的位置,或者您喜欢的任何其他位置,只要它是用户唯一的即可。
VAL 然后将尝试通过工作组的副本登录,该副本现在将只包含单个用户的单个记录锁定。VAL 还将 Environment.UserName
传递给工作组登录,并尝试将用户登录到工作组。这确保了 Access 数据库的 LDB 文件包含诸如 SERVER01 DMORLEY、SERVER01 ANOTHERUSER、SERVER01 THIRDUSER 等信息。
通过强制用户使用其账户名登录,即使使用相同的服务器瘦客户端,Access 数据库的共享 LDB 文件也将包含唯一的用户名标识符。
访问权限
管理访问权限可能是一个繁琐的过程。如果您在 VAL 中设置用户,确保该用户也在您的网络工作组中创建并分配了组成员资格,这又增加了一层复杂性。
VAL 允许您从现有用户克隆新用户。除了为用户创建所有图标外,它还将尝试从源用户复制 Access 数据库权限,确保正确分配用户帐户和权限。
这是通过 ADODB 和 ADOX 实现的;请参阅源文件 WorkgroupHelper
,它使用反射来“后期绑定”COM 功能。使用“新用户”向导逐步完成克隆过程。
Access 数据库 - 总结
网络工作组的工作组设置应在 app.config VAL 设置的 enterpriseWorkgroupDirectory
和 enterpriseWorkgroupName
中定义。仅当您使用旧版 Access 数据库安全模型时才定义这些,否则可以留空。
互联网链接
VAL 也可用于将网站地址作为图标分发。例如,您可能有一些内部的内部网应用程序,可以从特定的 URL 访问。单个用户必须在浏览器收藏夹等地方记录这些 URL。
在 VAL 中,您可以创建一个新文件并选择要启动的浏览器,例如 C:\Program Files\Internet Explorer\iexplore.exe。VAL 会自动检索 Internet Explorer 图标并将其分配给您的文件。现在,您可以将其替换为任何您选择的图像,然后在文件维护屏幕的命令行部分创建一个条目,并将其指向您喜欢的任何 URL。想象一下,我们想要一个“CodeProject”的图标,您将输入 https://codeproject.org.cn/。
当 VAL 尝试启动此应用程序时,它将构建以下字符串来启动应用程序:"C:\Program Files\Internet Explorer\iexplore.exe" https://codeproject.org.cn/。
这是一种非常简单的方式,可以将 Web 应用程序作为代表您链接的图标分发给用户。如果任何 URL 发生更改,您都可以集中维护数据。
分发文件 - 一般
您始终可以分发文件,无论其底层文件类型如何。只要运行 VAL 客户端的帐户有权访问文件维护屏幕中指定的位置,它就可以复制文件并将其放置在另一个位置。
用户希望通过文件副本启动的原因有很多,VAL 让您轻松管理这一切。
摘要
这是可视化应用程序启动器及其可能用途的概述。正如文章开头所提到的,对于这样一个简单的应用程序来说,它是一个过度设计的解决方案,但这里的真正目的是展示我们如何利用我们可用的各种工具。
致谢
像大多数程序员一样,我从互联网资源和示例项目中学到了很多东西。这个解决方案中的许多代码可能以某种形式在互联网上找到。如果我使用了完整的类,任何标题和信用都会指向原始作者。在其他情况下,我在留言板上找到了片段和解决方案,并将它们合并到更大的类中。在这种情况下,我无法感谢所有人,因为我已经忘记了所有东西的来源!
非常感谢那些从繁忙的日程中抽出时间回答问题、指引我们寻找资源或仅仅是开源项目的人们。
待办事项
下一步是创建一个 WPF 前端而不是 WinForms,并让它使用相同的服务模型。那将会在未来的文章中介绍 :)
历史
- 2012年12月29日 - Entity Framework 更新和代码简化。
- 将 Entity Framework 更新到 4.2 版的“Code First”语法。
- 移除了仓储项目/工作单元实现。现在使用单一泛型仓储和 DbContext API。
- 移除了所有具体的仓储和仓储特性。方法更简单、更简洁。
- 添加了 NuGet 包。
- 添加了 Moq 框架并更新了所有测试以使用 Moq。
- 2011年9月15日:更新文章以反映2011年6月24日所做的更改。
- 删除了 WCF 服务调用器和 ChannelFactoryManager 的详细信息,现已过时。
- 添加了关于每个请求工作单元模式以及如何从 StructureMap 控制此模式的详细信息。
- 添加了关于自定义安全属性的信息。
- 2011年6月24日:项目整体结构发生重大变化。
- 添加了
IDispatchMessageInspector
的实现,以处理 WCF 的“之前”和“之后”事件。 - 工作单元现在通过
IDispatchMessageInspector
控制,采用“每个请求一个工作单元”的方法。简化了实际 WCF 服务中的代码。 - 将 WCF 服务简化为两个 svc 文件,一个“用户”服务和一个“管理员”服务。
- 简化了领域服务和契约,删除了不必要的类。
- 为管理员服务添加了基于属性的安全性。
- 删除了 WCF 服务调用器代理生成器。现在通过
ClientBase<>
访问 WCF 服务。 - 总体上整理了项目的组织并进行了其他各种改进:)
- 2011年3月25日 - 编辑。语法和可读性。
- 2011年3月24日 - 首次发布。