现在的位置: 首页 > 综合 > 正文

如何开发既适用于计算机又适用于Mobile平台的运行程序

2012年09月01日 ⁄ 综合 ⁄ 共 15290字 ⁄ 字号 评论关闭
转自:http://msdn.microsoft.com/zh-cn/magazine/cc163387.aspx
共享代码
编写同时适用于移动和桌面应用程序的代码
Daniel Moth

本文讨论:

  • Windows Mobile 和 Windows 桌面开发
  • 在平台间迁移应用程序
  • 可重定目标的程序集
  • 在平台间共享代码
本文使用了以下技术:
.NET Framework

下载本文中所用的代码: NetCfSharing2007_07.exe (246 KB)
浏览在线代码

在过去几年,尽管开发人员一直在为 Windows® 构建 Microsoft® .NET Framework 客户端应用程序,但是许多人都不知道他们还可以使用相同的技能和工具集为 Windows Mobile® 创建应用程序。但是 Windows Mobile 当时还没有在企业内广泛使用,因此编写面向移动设备的自定义应用程序的需求还不是很大。如今,为了满足大量的需求,许多桌面开发人员开始涉足移动开发。遗憾的是,尽管跨平台共享 .NET 代码相当容易,但许多这样的机会都没有得到利用。
无论原始应用程序是为 Windows 还是为 Windows Mobile 编写的,有很多原因可能会促使您跨这两种平台共享业务逻辑。您可以从其中任一个平台开始,迁移到另一个平台。如果应用程序当前是在现场的便携式计算机上运行,则在移动设备上运行可能是个不错的选择,反之亦然。如果您的产品与竞争者的产品类似,通过将其扩展到新的平台,可以使您在竞争中占据优势。还有一个额外的好处,就是通过编写在桌面上执行的代码,您可以利用不能用于智能设备项目的开发人员工具。
编写面向具有不同外观造型(不同的屏幕尺寸、方向、触摸屏等)的 Windows 设备的跨设备代码应用程序时,可以利用本文中介绍的技术和原理。

平台差异
当我想到 .NET Framework 时,我会想到工具、语言、库和运行时引擎。这四种元素中的差异会影响编写跨平台代码的目标。幸运的是,我们对桌面和移动应用程序所使用的工具是相同的:Visual Studio®。对于 Visual Studio 的每个版本,Visual Studio for Devices 团队继续添加用于设备开发的新功能,并确保重要的新桌面开发功能对于移动设备开发也可以正常使用。(对于设备开发,您需要使用 Visual Studio 2005 Standard 或更高版本。)
从语法角度看,两种平台都完全支持 Visual Basic® 和 C#。但是如果您计划使用 Visual Basic,我们需要提醒您 Microsoft.VisualBasic.Compatibility 程序集和隐式后期绑定不受支持。前者是对从 Visual Basic 6.0 迁移的桌面项目使用的程序集;如果您通过在 Visual Basic 项目中启用 Option Strict On 来遵循最佳实践,后者不是问题。还要意识到托管 C++ 也不受支持(虽然相同的 IDE 可用于本机设备项目)。通过引用正确的程序集和确保其编译器不生成 .NET Framework 精简版公共语言运行库 (CLR) 不支持的 IL 操作码,第三方语言可以添加对 .NET Framework 精简版的支持。冲突的 IL 操作码包括:jmp、calli、refanyval、refanytype、mkrefany、arglist、localloc、unaligned 和 tail。
需要注意的是,尽管垃圾收集器和实时 (JIT) 编译器的实现在 .NET Framework 精简版中与在 .NET Framework 中不同,但是它们仍然起到相同的作用并为托管代码提供相同的服务。
与任何主流编程框架一样,.NET Framework 附带有大量类库。.NET Framework 精简版仅占用了 20% 的资源就实现了 .NET Framework 中 80% 的相关功能。与 .NET Framework 相比,.NET Framework 精简版中不包含的主要方面包括:ASP.NET、CLR 宿主、代码访问安全 (CAS)、二进制序列化、Reflection.Emit 以及 CodeDOM。
Windows Presentation Foundation 和 Windows Workflow Foundation 很快就将不受支持,但是 .NET Framework 精简版 3.5 版中将提供 Windows Communication Foundation 的精简版。在 3.5 版本中,还将支持 LINQ to Objects、LINQ to XML 以及 LINQ to DataSet,但是不支持任何其他 LINQ 类型。
然而,大多数情况下,并不是因为缺少这些大的方面而导致出现问题。通常,问题是由于某些受支持的命名空间不具备您所需的所有类引起的,更重要的是,某些类不具备您所需的所有成员。稍后,我将介绍一些解决方法。
反之也是如此。也就是说,.NET Framework 精简版从严格意义上说不是 .NET Framework 的子集;它实际上添加了一些自己的成员。有四种特定于设备的程序集:System.Data.SqlServerCe、System.Net.IrDA、Microsoft.WindowsMobile.DirectX 以及 Microsoft.WindowsCE.Forms。
SqlServerCe 包含的类用于处理设备上的内存数据库;IrDA 包含的类用于针对红外线进行编程;DirectX® 用于处理丰富图形,主要适用于游戏编程;Forms 拥有大量特定于设备的 GUI 类。除了这些特定于设备的程序集之外,还有一些适用于 Windows Mobile 5.0 和更高版本的特定于设备的 Windows Mobile API,它们随 Mobile 平台一起提供。稍后,我将向您介绍如何处理在一种框架中存在,在另一种框架中不存在的类。
当然,桌面和移动设备上的输入方法是不同的,因此,您不必担心移动设备上的键盘快捷方式、工具提示等。此外,移动窗体不能调整大小,始终是全屏,这也会影响您在应用程序的窗体间导航的方式。最后,最明显的差别是设备的屏幕尺寸要小得多。为了消除这种影响,最好不要在这两种平台间共享应用程序的 UI 层,而是创建特定于平台的用户界面,仅共享业务逻辑。

如果无法继续进行怎么办
尝试将应用程序迁移到移动设备时,桌面开发人员通常做的第一件事是根据桌面框架构建程序集并从设备项目中引用该程序集。这样做是行不通的,绝对不支持这样做。即使在桌面项目中,您已经很小心,只使用 .NET Framework 精简版中也提供的类和成员,在运行时依然会出现 TypeLoadException 和 MissingMethodException 异常。因此,如果您通过选择非智能设备项目模板创建了 Visual Studio 项目,该项目的输出简直不能在 Windows CE 平台(当然,这是 Windows Mobile 的基础)上执行。
遗憾的是,您有时就面临上述情形。从智能设备项目引用程序集时,不会检查它是不是桌面程序集,因此您可以使用从桌面项目模板中构建的类库。当使用第三方 DLL 时,经常出现这种情况。在这种情况下,您只能到运行时才会发现错误。
如果您怀疑遇到了这一问题,有一些线索可供查找问题。通过 Visual Studio 将项目部署到目标位置时(如通过调试运行项目时),请注意在 Visual Studio 输出窗口中,诊断消息会告知您哪些程序集正在部署到设备(请参见图 1)。如果部署过程花费的时间很长,可能是因为 Visual Studio 正在将它检测为您意外引用的桌面程序集依赖关系的全套 .NET Framework 程序集复制到目标位置。
图 1 Visual Studio 输出窗口 (单击该图像获得较大视图)
如果您幸运的话,部署过程在失败时会显示通知您磁盘空间不足的错误。如果桌面依赖关系有限,则部署过程会成功。在这种情况下,可以使用显示所有文件类型的远程文件查看器,查看部署应用程序的目标位置上的文件夹。您将看到所有部署的 .NET Framework 程序集。注意,.NET Framework 精简版程序集的公钥令牌以 9 开头,而桌面的则以 B 开头。您可以使用这一信息来快速确定您所部署或引用的程序集是桌面还是设备框架程序集。该信息还会出现在 .NET Framework 精简版加载程序日志中,如果应用程序因为这些问题在运行时失败,该日志中将包含错误。

可重定目标
尽管桌面程序集不能在设备上执行,但好消息是:反之是可以的。如果您创建了智能设备项目并对其进行了构建,则输出(EXE 或 DLL)可以在桌面上执行,并且可以由桌面项目引用。这是因为 .NET Framework 精简版程序集可重定目标。如果您使用 ILDASM 打开其中一个程序集,您可以观察到它们具有 System.Reflection.AssemblyNameFlags.Retargetable 属性(请参见图 2)。这意味着在运行时所有对 .NET Framework 精简版程序集的引用都可以重定目标到桌面程序集。因此,需要说明的是,您的程序集使用的是 .NET Framework 的桌面实现,而不是设备实现。
图 2 ILDASM 中的可重定目标标志 (单击该图像获得较大视图)
让我们看一个您可以轻松尝试的示例。创建一个面向您最喜欢的 Windows Mobile 平台的新智能设备应用程序。向主窗体添加一些控件,例如 TabControl、ProgressBar,并向 MainMenu 控件添加四个 MenuItem(请参见图 3)。对每个 MenuItem 生成 Click 事件处理程序方法,并在这些事件处理程序方法中添加图 4 中的代码。

private void mnuFormShow_Click(object sender, EventArgs e)
{
// TODO later
}

 

private void mnuMbox_Click(object sender, EventArgs e)
{
MessageBox.Show(“some text”, “random caption”);
}

private void mnuDateTime_Click(object sender, EventArgs e)
{
this.Text = DateTime.Now.TimeOfDay.ToString();
}

private void mnuCalc_Click(object sender, EventArgs e)
{
string pth = @”\Windows\calc.exe”;
Process.Start(pth, null);
}

 

图 3 MainMenu 设计 
运行面向仿真器或您自己的设备的项目,并观察在单击每个菜单项时的行为是否与您期望的一样。返回到桌面,导航到构建文件夹 (bin/Debug),注意观察双击可执行文件后其在桌面计算机上的运行情况(请参见图 5)。
图 5 设备项目 
各控件按照桌面样式呈现 — TabControl 在顶部显示选项卡,菜单出现在顶部,ProgressBar 的外观略有不同,等等。现在单击显示 MessageBox 的 MenuItem,它按预期方式运行。单击更改窗体标题的 MenuItem,注意我们如何通过添加毫秒在此处获得更高精确度。这也可以进一步说明您现在使用的是有一点点不同的桌面实现。至此的结论是不要完全依赖于可重定目标性,而是要全面测试应用程序以确定实现中的任何微小差别对您都不太重要。最后,单击启动计算器的 MenuItem 并观察应用程序如何崩溃,并显示“System.ComponentModel.Win32Exception:系统找不到指定的文件”异常。显然,出现这种情况的主要原因是桌面上的计算器路径与设备上的计算器路径不同。要解决此类特定于平台的问题并提供可以跨平台作业的单一实现,您需要在运行时检测所在的平台并相应地编写代码的分支。请将以下对事件处理程序所做的修改作为参考示例:

private void mnuCalc_Click(
object sender, EventArgs e)
{
string pth;
if (Environment.OSVersion.Platform == PlatformID.WinCE)
{
pth = @”\Windows\calc.exe”;
}
else
{
pth = @”c:\Windows\system32\calc.exe”;
}
Process.Start(pth, null);
}

您现在可以在 Windows Mobile 或 Windows 桌面上运行该项目,对应于该平台的计算器应用程序将可以正确启动。
请记住,有一些特定于设备的程序集在桌面上不存在。让我们看一下使用此类程序集将发生什么情况。向智能设备项目添加一个新窗体 (Form2),并在其中添加 InputPanel 控件,然后添加两个 Button 控件和一个 TextBox。对控件生成事件处理程序并添加图 6 中所示的代码。

private void textBox1_GotFocus(object sender, EventArgs e)
{
inputPanel1.Enabled = true;
}

 

private void textBox1_LostFocus(object sender, EventArgs e)
{
inputPanel1.Enabled = false;
}

private void button1_Click(object sender, EventArgs e)
{
Cursor.Current = Cursors.WaitCursor;
}

private void button2_Click(object sender, EventArgs e)
{
Cursor.Current = Cursors.Default;
}

 

现在重新访问第一个窗体的空 MenuItem Click 事件处理程序方法并添加显示 Form2 的代码,如下所示:

private void mnuFormShow_Click(object sender, EventArgs e)
{
Form2 f = new Form2();
f.ShowDialog();
}

如果该在设备上运行项目,您会注意到从 Form1 的菜单项打开 Form2 时,您可以单击 Form2 的按钮以在设备上显示或隐藏忙状态的光标。将焦点转移到 Textbox 将打开软输入面板 (SIP),而将焦点移走会隐藏该面板。返回到桌面,像前面一样导航到构建目录,并在桌面上运行可执行文件。单击显示 Form2 的菜单项将导致出现异常,如下所示:

System.IO.FileNotFoundException: Could not load file or assembly
‘Microsoft.WindowsCE.Forms, Version=2.0.0.0, Culture=neutral,

PublicKeyToken=969db8053d3322ac’ or one of its dependencies.
The system cannot find the file specified. File name: ‘Microsoft.WindowsCE.Forms,
Version=2.0.0.0, Culture=neutral, PublicKeyToken=969db8053d3322ac’

发生这种情况,是因为向项目添加 InputPanel 控件时,会自动添加对 Microsoft.WindowsCE.Forms 的引用。它是仅设备程序集,所以在重定位到的桌面上并不存在,因此在构建窗体并尝试从该程序集创建 Microsoft.WindowsCE.Forms.InputPanel 时将引发异常。让我们做一些修改,以更好地进行观察。首先从该窗体的设计器视图删除 InputPanel 控件,然后修改 textBox 事件处理程序,如图 7 所示。

private void textBox1_GotFocus(object sender, EventArgs e)
{
if (Environment.OSVersion.Platform == PlatformID.WinCE) //naive
{
Microsoft.WindowsCE.Forms.InputPanel inputPanel1 =
new Microsoft.WindowsCE.Forms.InputPanel();
inputPanel1.Enabled = true;
}
}

 

private void textBox1_LostFocus(object sender, EventArgs e)
{
// in the interest of space, let’s ignore this method
//inputPanel1.Enabled = true;
}

 

此操作所做的是将 InputPanel 的使用分离到一种方法中,并且它还通过在运行时检查平台尝试在本地修复异常如果您在设备上运行此项目,它仍然可以按照预期的方式运行,但是如果在桌面上运行,它会失败,并发生与前面相同的错误。它仅在 textBox1 获得焦点时失败,而不是一显示窗体就会失败,但是无论如何它终究会失败。
由于 JIT 编译,仅进行运行时检查在这种情况下是不够的。在运行时调用方法时,CLR 会检查该方法的现有任何本地代码是否正在被调用 (textBox1_GotFocus)。如果没有,则 JIT 编译器将从此方法入口指向的 IL 代码生成本地操作码。此时,JIT 编译器不管在运行时从什么路径执行,都只需要查找此方法中使用的所有类型。因此,它尝试加载 Microsoft.WindowsCE.Forms.InputPanel 类型和对应的程序集,但是却无法完成,因为程序集在桌面上不存在。然后就引发了异常。您可以将对 InputPanel 的使用提取到一个单独的方法中,并通过对调用方法的运行时检查确保提取的方法永远不被调用,但是这种方法实在比较乱,大多数开发人员都不喜欢。解决此问题的正确方式是探求其他方法跨平台共享代码。(如果您对上面提到的方法感兴趣,请查看 msdn2.microsoft.com/ aa480686 上的“平台检测”部分,其中提供了一个与上述方法相似的分解方法的示例。)

共享代码(而非二进制文件)
上一部分中说明了如何创建智能设备项目以及如何通过重定位在桌面上使用输出二进制文件。然而,可重定位目标方法有两个限制。首先,在代码中不能使用任何仅适用于桌面的类型或方法。它们无法进行编译。第二,必须小心不能在桌面上加载任何特定于设备的程序集,因为它们会因出现运行时异常而失败。
这两个问题都可以通过以下方式解决:在编译时而非运行时使用代码的条件分支,创建两个项目,使其具有两个单独输出以便获得使用一个源代码文件集的特定于平台的程序集。后者很重要,因为最终目标是将对业务逻辑的所有更改同时传播给两个平台,而不产生任何易于出现复制/粘贴错误的开销。让我们尝试一下另一种方法。
首先,创建一个新 Windows 桌面项目。删除默认添加的代码文件(Form1 和 Program 文件)。从项目菜单中选择“添加现有项...”,并浏览到以前创建的智能设备项目文件夹,代码文件就驻留在其中。最后,仅选择所有文件名形式为 *.cs 的代码文件并将其作为链接添加到项目。这很重要,因为如果选择对话框的默认“添加”选项,您将会创建副本而不是链接到这些文件(请参见图 8)。
图 8 添加现有文件 
构建项目,注意在编译时现在会出现以前仅在运行时才出现的错误(请参见图 9)。当然,在这种情况下添加对特定于设备的程序集的引用没有用。因此,我将在此介绍条件编译。
图 9 在编译时发现问题 (单击该图像获得较大视图)
转到桌面项目属性,在“构建”选项卡上添加名为 FULL_FRAME(即“full framework”的简略形式)的新“条件编译符号”,如图 10 所示。
图 10 条件编译常量 (单击该图像获得较大视图)
接下来,使用新引入的编译常量修正 Form2.cs 中的编译错误。通过以下方法修改冲突方法,然后重新构建桌面项目:

private void textBox1_GotFocus(object sender, EventArgs e)
{
#if !FULL_FRAME
Microsoft.WindowsCE.Forms.InputPanel inputPanel1 =
new Microsoft.WindowsCE.Forms.InputPanel();
inputPanel1.Enabled = true;
#endif
}

构建此(桌面)项目后,您可以运行该项目,查看是否一切都按照预期的方式工作。如果切换到设备项目,请注意该项目如何提示您源文件已在 Visual Studio 实例外部发生更改并询问您是否要重新对其进行加载;单击“是”。
因为这是一个使用单独条件编译常量集的单独项目,所以该项目将可以在设备上正常编译和运行 — 您不再需要在桌面上运行此项目的输出!这意味着您可以更改以前在启动计算器时引入的运行时条件,使其成为编译时条件,从而减小方法的大小。
现在,您已经了解了使用条件编译的机制,让我们来思考一下它的好处。拥有两个单独的项目意味着您在每个项目中可以具有不同的项目引用集。您还可以在一个项目中包含代码文件,在另一个中不包含。在桌面端,您可以利用完整框架的成员来为桌面用户创建良好的体验;只是要确保对这些桌面成员的使用要以 #if 处理器指令结束,或者分离到仅在桌面项目中存在的源文件中。在很多示例中,您可以使用完整框架成员增强在桌面端的体验,我们来看一个示例。
在 Program.cs 文件中,按照以下方式修改 Main 函数:

static void Main()
{
#if FULL_FRAME
Application.EnableVisualStyles();
#endif
Application.Run(new Form1());
}

现在运行项目,将原始控件的外观与通过可重定位目标方法得到的控件外观进行比较(请参见图 11)。
最后,敏锐的您会发现显示和隐藏光标的代码在桌面上不能运行(即使不会出现任何编译或运行时错误)。还要注意,从 Visual Studio 2005 开始,设备项目已具有一些已经定义的条件编译常量:分别适用于 PocketPC、Smartphone、Windows CE。您可以选择仅使用内置的常量,也可以选择使用自定义的常量(如刚刚介绍的 FULL_FRAME),还可以同时使用内置常量和自定义常量 — 这一点关系都没有。
图 11 原始控件和重定位控件 (单击该图像获得较大视图)
了解了跨平台共享代码资源后,应该了解一些提示和技巧。其中一些适用于可重定位目标方法,一些适用于条件编译,还有一些对两者都适用。

同一目录中的项目和代码文件
共享代码时,我创建了两个单独的项目,并且以链接形式将第一个项目的源代码文件添加到了第二个中。我建议将所有的源代码文件与项目文件放在同一目录中,这样可以根据需要很轻松地在 Visual Studio 解决方案浏览器中包含或排除这些文件。为了实现这个目标,在创建设备项目后,创建一个 Windows 桌面项目并像前面一样删除代码文件。不要再添加任何文件,关闭 Visual Studio 并导航到创建桌面项目文件的目录。剪切项目文件并将其粘贴到设备项目文件所在的目录。例如,如果设备项目位于“C:\Temp\”,则将桌面项目文件 (csproj) 剪切并粘贴到该文件夹(以确保桌面和设备项目具有不同的名称)。
现在打开桌面项目并在解决方案浏览器中“显示所有文件”。您可以看到所有设备源代码文件,并将其纳入您认为适用的项目中 — 如果您打开设备项目,也是一样(请参见图 12)。
图 12 选择文件 
通过这种方法,向一个项目添加新文件时,即使打开的是其他项目类型也可以很轻松地找到,并且可以更简单地包含所需的文件。
请注意,项目和输出程序集名称是不同的,但是命名空间和类型名称应保持相同。如果您希望两个平台使用相同的程序集名称,则需要在项目属性中重命名构建文件夹,例如 binCF 和 binFF。我还建议任何时候都只打开一个项目,这样可以避免产生混淆,特别适合源代码管理。

为代码添加存根
很多时候,最好不要使用条件编译使代码显得杂乱,而应该对不适用于平台的代码添加虚拟空存根。例如,如果您在项目中总是使用某个类,并且该类不能在其他平台上使用,则您可以使用相同的(但是为空)的接口添加将该类。下面是使用 InputPanel 类的一个示例:

namespace Microsoft.WindowsCE.Forms
{
public class InputPanel
{
public bool Enabled;
}
}

现在,在任何位置使用 InputPanel,都可以使用相同的代码而不需要条件分支,这对桌面平台没有任何影响。

平台调用
很多 .NET Framework 精简版应用程序都可以通过使用 P/Invoke 服务访问驻留在本机 DLL 中的功能,从而实现其他功能。通常,这些 DLL 的名称在 Windows CE 和桌面版本的 Windows 上不同。根据前面您选择的方法,可以通过两种方法处理。如果使用的是条件编译,图 13 中的代码说明了如何编写跨平台的 P/Invoke 声明。如果选择的是可重定目标方法,图 14 中的方法很有用。

class PInvokesRuntime
{
[DllImport(“coredll.dll”, EntryPoint = “QueryPerformanceCounter”)]
internal static extern int QueryPerformanceCounterCE(
out Int64 perfCounter);

 

[DllImport(“kernel32.dll”, EntryPoint = “QueryPerformanceCounter”)]
internal static extern int QueryPerformanceCounterFull(
out Int64 perfCounter);

private void UseIt()
{
long p;
int i;
if (Environment.OSVersion.Platform == PlatformID.WinCE)
{
i = QueryPerformanceCounterCE(out p);
}
else
{
i = QueryPerformanceCounterFull(out p);
}
}
}

 

class PInvokesCompileTime
{
#if FULL_FRAME
private const string DllName = “kernel32.dll”;
#else
private const string DllName = “coredll.dll”;
#endif

 

[DllImport(DllName)]
internal static extern int QueryPerformanceCounter(
out Int64 perfCounter);

private void UseIt()
{
long p;
int i;
i = QueryPerformanceCounter(out p);
}
}

 

不同的框架实现
在前面的部分,您了解了 System.DateTime.TimeOfDay 如何根据使用的框架返回不同精确度的值。因此,您可以在测试之后决定如何在代码中进行相关处理(如果这对您来说很重要)。在某些其他情况下,各个方法具有不同的行为,尤其是在特定情况下,一个框架中的方法引发的异常会不同于另一个框架中的方法引发的异常,例如:

private void button1_Click(object sender, EventArgs e)
{
Type t = this.GetType();
MethodInfo m = t.GetMethod(“DoIt”);
m.Invoke(this, null); //throws
}

 

public void DoIt()
{
throw new InvalidOperationException(“my message”);
}

此代码中调用 MethodInfo 的一行在两个平台上都会引发异常。在设备上,引发的异常是 InvalidOperationException。然而,在桌面上引发的异常却是 TargetInvocationException(它将原始的 InvalidOperationException 集设置为 InnerException 属性)。因此,在测试过程中,您还应该测试异常情况,确保两种 Exception 类型中都有 Catch 块。
不同框架实现的另一个示例是在前面显示忙状态的光标时您看到的内容。对设备有影响的东西对桌面没有任何影响,桌面上需要的东西在设备上不受支持。如果您希望两个平台上的行为一致,可以提取一种方法,并在其中隔离封装光标显示的条件行为,如下所示:

public static class TheCursor 
{
public static void CursorCurrent(Control c, Cursor defaultOrWait)
{
#if FULL_FRAME
if (c != null) c.Cursor = defaultOrWait;
#else
Cursor.Current = defaultOrWait;
#endif
}
}

然后设备和桌面平台上使用的调用代码可以保持相同:

private void button1_Click(object sender, EventArgs e)
{
TheCursor.CursorCurrent(this, Cursors.WaitCursor);
}

 

private void button2_Click(object sender, EventArgs e)
{
TheCursor.CursorCurrent(this, Cursors.Default);
}

分部类型
在前面我提到过,如果源代码文件包含特定于平台的功能,您可以在一个项目中包含它们,而在另一个项目中不包含。在类适用于两种平台但是类的某些成员不适用于这两种平台时,会出现相似的情况。您不需要通过 #if 指令封装类的大部分,您可以使用 Visual Studio 2005 中引入的分部类功能。换句话说,就是使用局部关键字将类拆分为两个文件,以此将只适用于一个平台的功能分离到一个不同的文件中。然后只需在不适合的项目中排除该文件即可。

System.Diagnostics.Conditional
值得注意的一个提示是可以对不希望在运行时调用的方法使用 System.Diagnostics.ConditionalAttribute。对方法应用该属性的结果是任何对该方法的调用都不会包含在经过编译的二进制文件中。当然,应用了该属性的方法必须仍然能够编译:

[Conditional(“FULL_FRAME”)]
public void SomeDesktopOnlyMethod()
{
//TODO code that compiles in both projects
}

如果以 #if 结束整个方法,您必须查找所有调用该方法的位置,并使用 #if 封装调用。而此处,只需对不需要的方法应用该属性,而不用管所有调用的位置;这样就不会在二进制文件中对其进行编译。

部署到我的计算机
我们都知道,如果选择可重定目标方法,在运行时只能发现桌面平台错误。这就产生了如何在桌面上调试应用程序的问题,因为智能设备项目只能部署到仿真器或物理设备。您可以采取两种方法。
最简单但也是最乏味的方法是从文件系统运行该应用程序,然后通过“Visual Studio 工具”|“附加到进程”菜单对其附加调试程序(请参阅图 15)。您还可以通过创建桌面项目充当启动程序来半自动化此过程。向具有智能设备项目的解决方案添加桌面项目,将其设置为启动项目,然后引用此智能设备项目。确保该桌面项目只有一个程序文件且该文件只具有一个静态 Main 函数,并通过该函数将智能设备项目的启动窗体的一个新实例传递给 Application.Run 方法。
图 15 “附加到进程”对话框 (单击该图像获得较大视图)
如果智能设备项目的部署选项中具有“部署到我的计算机选项”,将是很理想的情况。事实上,在早期的 Community Technology Preview of Visual Studio 2005 期间,存在该选项(但是最终产品中删除了该选项,Visual Studio“Orcas”版本中也没有重新添加该选项)。
按照我的博客中的说明进行操作可以重新启用“部署到我的计算机”功能,但是请注意 Microsoft 不建议(或支持)这样做。

使用映像
如果选择使用可重定目标方法,则不能使用仅适用于桌面的方法,因为这些方法在设备项目中将无法编译。然而,如果确实需要,可以使用映像调用此类方法。重新回顾以前我希望为应用程序启用主题的示例,我可以通过以下方法实现:

static void Main()
{
//Application.EnableVisualStyles();
if (Environment.OSVersion.Platform != PlatformID.WinCE)
{
Type t = typeof(Application);
MethodInfo m = t.GetMethod(“EnableVisualStyles”);
m.Invoke(null, null);
}

// As before
Application.Run(new Form1());
}

扩展方法
在使用 .NET Framework 精简版类时,经常会出现这样的情况:与完整 Framework 相比,特定类缺少成员。通常,开发人员必须自己编写缺少的功能,然后决定是使用继承添加缺少的成员(如果可以),还是将其添加到实用程序帮助器类。例如,假设您要对与桌面共享的设备项目中的 TextBox 执行 Cut 操作。但 .NET Framework 精简版中的 TextBox 类中缺少 Cut 方法。根据在本文中学习的内容,您可以向设备项目添加类似图 16 所示的代码文件。

using System;
using System.Windows.Forms;
using Microsoft.WindowsCE.Forms;

 

static class TextBoxExtensions
{
const int WM_CUT = 0x0300;
const int WM_COPY = 0x0301;
const int WM_PASTE = 0x0302;

public static void Cut(TextBox t)
{
Message msg = Message.Create(
t.Handle, WM_CUT, IntPtr.Zero, IntPtr.Zero);
MessageWindow.SendMessage(ref msg);
}
}

 

图 16 中的文件不需要条件,因为它只呈现在设备项目中。然后您可以按照如下所示的内容编写调用代码(在两个项目中都将存在):

private void SomeMethod()  
{
#if FULL_FRAME
textBox1.Cut();
#else
TextBoxExtensions.Cut(textBox1);
#endif
}

C# 3.0 和即将发布的 Visual Basic 版本中有一个称为扩展方法的新功能。在此,我不对此功能进行全面地说明,不过,如果您对此功能不熟悉,可以访问我的博客或查看 Anson Horton 的文章(发表在《MSDN® 杂志》2007 年 6 月这一期上)。代号为“Orcas”的 Visual Studio 中随附的 .NET Framework 精简版 3.5 也支持此功能,该功能可以帮助解决上述情况。
在设备项目中,添加对 System.Core.dll 的引用,然后通过在帮助器方法的第一个参数前添加 this 关键字修改仅设备文件,以此使该方法成为扩展方法,如下所示:

public static void Cut(this TextBox t)

完成此更改后,调用代码在设备和桌面平台上可以相同,而且没有条件:

private void SomeMethod()  
{
textBox1.Cut();
}

在桌面上,将调用框架方法;在设备上,将调用扩展方法。如果将来 .NET Framework 精简版团队添加了此方法,则代码将无缝调用真正的 .NET Framework 方法(因为实例方法优先于扩展方法),在某个时候您可以删除多余的代码文件。


总结
在本文中我介绍了桌面开发和 Windows Mobile 开发之间的差异,总结出最重要的差异是在 Framework 库中,而不在其他地方。我提供了两个用于跨平台共享代码资源的方法示例,并表明我建议使用条件编译方法,此外还提出了一些有用的技巧。尽管我个人建议使用此知识共享类库中包含的业务逻辑,您可能还是希望共享 GUI,特别是在应用程序面向设备时,如 Ultra Mobile PC (UMPC) 或其他运行 Windows 桌面(而不是面向 Windows Mobile)的触摸屏设备)。


Daniel Moth在 Microsoft 在英国的开发人员和平台小组中工作。他是《Microsoft Mobile Development Handbook》(Microsoft Press, 2007) 的作者之一,您可以通过他在 www.danielmoth.com/Blog 上的博客与之联系。

 

抱歉!评论已关闭.