应用程序套件模板
一个使用反射和自定义属性动态发现和添加子应用程序的应用程序套件的示例。
引言
套件是打包和分发应用程序的一种方式,在通用框架中提供统一的外观和感觉,或者将功能分组到一个通用容器下。扩展套件以添加新应用程序或更新现有应用程序可能会遇到问题。我在这里演示的方法是使用动态运行时发现来从 DLL 中加载应用程序。
致谢
本项目中使用的 Outlook 栏来自 Marc Clifton 的文章 Outlook 栏实现。我做了一个小修改来支持一个额外的字段。
套件容器
套件容器是将所有内容组合在一起的外壳。它为应用程序提供统一的外观和感觉、菜单项,基本上是一个“栖身之所”。对于本项目,我创建了一个非常简单的容器,其中包含一个 Outlook 栏用于分组和显示已加载的应用程序,以及一个用于显示应用程序界面的区域。
加载应用程序
当套件容器启动时,它需要查找要加载的任何应用程序。一种方法是让它搜索给定路径下的任何 DLL 并尝试加载它们。这种方法显而易见的问题是,该路径可能包含不支持加载的 DLL。一种解决方法是使用自定义属性来指示 DLL 中的类(或类)是应用程序套件的一部分。
public static Hashtable FindApps()
{
// Create hashtable to fill in
Hashtable hashAssemblies = new Hashtable();
// Get the current application path
string strPath = Path.GetDirectoryName(ExecutablePath);
// Iterate through all dll's in this path
DirectoryInfo di = new DirectoryInfo(strPath);
foreach(FileInfo file in di.GetFiles("*.dll") )
{
// Load the assembly so we can query for info about it.
Assembly asm = Assembly.LoadFile(file.FullName);
// Iterate through each module in this assembly
foreach(Module mod in asm.GetModules() )
{
// Iterate through the types in this module
foreach( Type t in mod.GetTypes() )
{
// Check for the custom attribute
// and get the group and name
object[] attributes =
t.GetCustomAttributes(typeof(SuiteAppAttrib.SuiteAppAttribute),
true);
if( attributes.Length == 1 )
{
string strName =
((SuiteAppAttrib.SuiteAppAttribute)attributes[0]).Name;
string strGroup =
((SuiteAppAttrib.SuiteAppAttribute)attributes[0]).Group;
// Create a new app instance and add it to the list
SuiteApp app = new SuiteApp(t.Name,
file.FullName, strName, strGroup);
// Make sure the names sin't already being used
if( hashAssemblies.ContainsKey(t.Name) )
throw new Exception("Name already in use.");
hashAssemblies.Add(t.Name, app);
}
}
}
}
return hashAssemblies;
}
从这段代码可以看出,我们正在搜索应用程序路径下的任何 DLL,然后加载它们并检查是否存在我们的自定义属性。
t.GetCustomAttributes(typeof(SuiteAppAttribute), true);
如果找到,我们创建一个 SuiteApp
帮助类实例,并将其放入一个 Hashtable
中,以应用程序名称作为键。这将在稍后需要查找应用程序进行激活时发挥作用。这确实会限制不允许重复的应用程序名称,但为了避免用户混淆,这是一件好事。
使用特性
属性是向程序集、类、方法等传递信息的一种方式。有关它们的更多信息可以在这里找到,C# 程序员参考属性教程。在本项目的案例中,创建了一个自定义属性,并用它来提供套件加载找到的任何应用程序所需的两个信息。首先,仅通过查询属性的存在,我们就可以知道应该加载 DLL。我们从属性中获得的第二部分信息是应该将其添加到哪个 Outlook 栏组以及要显示的应用程序名称。
[AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]
public class SuiteAppAttribute : Attribute
{
private string m_strName;
private string m_strGroup;
/// <SUMMARY>
/// Ctor
/// </SUMMARY>
/// <PARAM name="strName">App name</PARAM>
/// <PARAM name="strGroup">Group name</PARAM>
public SuiteAppAttribute(string strName, string strGroup)
{
m_strName = strName;
m_strGroup = strGroup;
}
/// <SUMMARY>
/// Name of application
/// </SUMMARY>
public string Name
{
get{ return m_strName; }
}
/// <SUMMARY>
/// Name of group to which the app
/// should be assigned
/// </SUMMARY>
public string Group
{
get{ return m_strGroup; }
}
}
这里首先要注意的是应用于此自定义属性的属性。
[AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]
这告诉运行时,此特定的自定义属性只能应用于类,并且只能应用一次。
创建支持的应用程序
要创建成为我们套件一部分的应用程序,我们需要从 Build
事件开始。为了方便调试,应用程序必须移动到一个套件容器可以检测和加载的位置。此步骤可以通过添加生成后步骤来自动化。
copy $(TargetDir)$(TargetFileName) $(SolutionDir)SuiteAppContainer\bin\debug
假设所有内容都在同一个解决方案中,这将把应用程序编译步骤的输出复制到套件容器的调试文件夹中。
激活应用程序
要激活应用程序(一旦选择了图标),我们首先检查它是否确实存在于 HashTable
中,如果不存在则存在一些实际问题。我们还需要确保窗体尚未创建。在验证了这些检查之后,将定位并加载程序集的路径。InvokeMember
函数用于创建相关窗体的实例。我们为窗体关闭事件设置了一个处理程序,以便稍后可以像我们看到的那样进行处理。
public void OnSelectApp(object sender, EventArgs e)
{
OutlookBar.PanelIcon panel = ((Control)sender).Tag as
OutlookBar.PanelIcon;
// Get the item clicked
string strItem = panel.AppName;
// Make sure the app is in the list
if( m_hashApps.ContainsKey(strItem) )
{
// If the windows hasn't already been created do it now
if( ((SuiteApp)m_hashApps[strItem]).Form == null )
{
// Load the assembly
SuiteApp app = (SuiteApp)m_hashApps[strItem];
Assembly asm = Assembly.LoadFile(app.Path);
Type[] types = asm.GetTypes();
// Create the application instance
Form frm = (Form)Activator.CreateInstance(types[0]);
// Set the parameters and show
frm.MdiParent = this;
frm.Show();
// Set the form closing event so we can handle it
frm.Closing += new
CancelEventHandler(ChildFormClosing);
// Save the form for later use
((SuiteApp)m_hashApps[strItem]).Form = frm;
// We're done for now
return;
}
else
{
// Form exists so we just need to activate it
((SuiteApp)m_hashApps[strItem]).Form.Activate();
}
}
else
throw new Exception("Application not found");
}
如果窗体已存在,那么我们只想激活它,将其带到前面。
((SuiteApp)m_hashApps[strItem]).Form.Activate();
窗体关闭
我们需要能够捕获子窗体关闭的时间,以便释放资源,并且当需要再次使用时,将重新创建窗体,而不是尝试激活一个已经关闭的窗体。
private void ChildFormClosing(object sender, CancelEventArgs e)
{
string strName = ((Form)sender).Text;
// If the app is in the list then null it
if( m_hashApps.ContainsKey(strName) )
((SuiteApp)m_hashApps[strName]).Form = null;
}
菜单
接下来要解决的领域是菜单。主容器应用程序具有基本的菜单结构,它所包含的每个应用程序都将拥有自己的菜单,其中一些菜单项是相同的。
合并菜单并不难,只需要注意细节和计划。菜单属性 MergeType
和 MergeOrder
用于确定菜单如何合并以及项目出现的位置。默认设置是 MergeType = Add
和 MergeOrder = 0
。在本例中,我们希望将容器应用程序的“文件”菜单与子应用程序的“文件”菜单合并。首先,我们需要将主窗口文件菜单的 MergeType
设置为 MergeItems
。子应用程序中的“文件”菜单也必须设置为 MergeItems
。
这稍微接近一些,但正如所见,其中一些项目存在重复。我已经重命名了子项的退出菜单,以更好地说明问题。为了纠正这一点,我们需要将主窗口的退出菜单项更改为 MergeType = Replace
。
现在,我们得到了预期的结果。下一步是设置 MenuOrder
。这不会影响菜单的合并,但会影响项目出现的位置。查看示例项目,我们可以看到退出菜单项的 MergeOrder
为 99。合并从 0 开始,数字越高的项目合并在菜单的较低位置。
通过将 MergeOrder
设置为 0,我们可以看到退出菜单项位于文件菜单的较高位置。
结论
这肯定不是一个惊天动地的杀手级应用程序。我希望它是一个学习工具,可以探索功能并激发思考,也许其中一些技术可以被纳入其他应用程序。