65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 .NET 开发 Android 应用程序

2011 年 5 月 13 日

CPOL

9分钟阅读

viewsIcon

119330

本文介绍了 Resco MobileForms Toolkit, Android Edition。除了组件的简要特性外,重点介绍了使用主/明细概念编写 LOB 应用程序。

本文介绍了 Resco MobileForms Toolkit, Android Edition。除了组件的简要特性外,重点介绍了使用主/明细概念编写 LOB 应用程序。

引言

根据 Gartner 的最新研究,Android 操作系统在 2012 年应拥有 50% 的移动市场份额。

Android 开发者主要使用 Java 和基于 Google 的 Java 库。虽然这听起来像是一种跨平台策略,但现实情况却有所不同。

  • Android 不使用成熟的 Java 标准,即 Java SE 和 ME。
  • Java 在开发者中的受欢迎程度正随着时间的推移而显著下降。取而代之的是,毫无疑问最受欢迎的编程语言是 C (C, C++, C# 和 Objective-C)。

2011 年 4 月 6 日,Novell 宣布了其备受期待的 **Mono for Android** 的发布。Mono 是一种工具,使移动程序员能够使用 C# 和 Microsoft Visual Studio 创建基于 .NET 的应用程序,这些应用程序可在 Android 手机和平板电脑上运行。

这可以节省大量时间和金钱,因为开发人员可以共享跨多个平台的通用代码,包括 Android、iOS(使用 MonoTouch)、Windows Phone 7、Windows 桌面和 Windows 服务器。

Resco MobileForms Toolkit

该工具包有着悠久的历史。它最初是为 Windows Mobile 平台构建的,在那里它提供了一系列出色的控件,帮助成千上万(主要是企业)开发人员构建具有吸引力用户界面的移动应用程序。

鉴于非常受限的平台支持(.NET Compact Framework),这并不容易实现,该框架主要为桌面编程而构建。
该工具包的起源受到基于主/明细原理的 LOB 应用程序的启发。这催生了 AdvancedList 和 DetailView 控件,随后又出现了数十种其他更专业的控件。

该工具包之所以流行,是因为它主要为移动开发而构建。虽然今天听起来可能微不足道,但几年前并非如此。
通过使用该工具包,开发人员可以利用他们在桌面平台上获得的 .NET/C# 经验;他们可以相对轻松地构建移动 LOB 应用程序,而无需深入研究复杂的平台细节。

正是这群 Windows Mobile 开发人员将发现该工具包的 Android 版本非常熟悉,并且能够非常快速地使用它。那些从未用过 Resco 工具包的人可以从下面的屏幕截图示例中获得灵感。

必备组件

目标受众是 C# .NET 程序员。如果读者对 Android 编程有基本了解会更好,但——严格来说——这并非必需。
IT 决策者可以阅读本文(主要是第一部分),以大致了解使用该工具包可以做什么。

想要积极测试该工具包的程序员需要安装 Mono for Android。(http://mono-android.net/Installation

第一部分 - 工具包概览

主/明细场景

Resco MobileForms ToolkitResco MobileForms Toolkit

上图展示了借助 AdvancedList(左图)和 DetailView 控件(右图)构建的主/明细场景。我们将在文章的第二部分演示这两个控件的编程。在此,让我们笼统地介绍一下它们的功能。

AdvancedList 控件显示对象列表,在本例中是客户。每个列表行根据程序员指定的模板显示一个客户对象。如上图所示,普通模板显示客户姓名和地址。选定的客户(您点击的客户)使用另一个具有不同颜色和附加操作按钮的模板。

程序员必须提供列表的数据源(任何对象集合)、设计数据模板(选择要显示的数据属性、它们的格式等)、编写按钮处理程序。在上图所示的例子中,“更多”按钮的处理程序会启动客户详细信息编辑器,即 DetailView 实例。

行模板是单元格的集合。每个单元格都有位置(行内的边界框)、样式(字体、颜色等)和单元格数据。以下是典型的单元格示例:

  • 按钮单元格
  • 具有常量(字符串)内容的文本单元格
  • 显示数据对象属性的文本单元格(数据绑定)
  • 显示 object.ToString() 值的文本单元格。(当未提供属性名时发生此情况。)
  • 具有位图内容的图像单元格

另一个可选的列表功能是过滤。程序员必须编写过滤逻辑,该逻辑接受用户文本并返回修改后的项目列表。支持多个过滤器。

从用户的角度来看,过滤是什么样的?用户在搜索栏中输入文本,然后从(单级或两级)过滤器菜单中选择过滤器。下面的屏幕截图演示了 2 级过滤。

Resco MobileForms ToolkitResco MobileForms Toolkit

DetailView 控件显示项目列表。每个项目由标签和数据编辑器组成,并可以通过多种方式进行格式化(字体、颜色、对齐方式…)。您可以从七种预定义的项目编辑器中选择(或提供自己的自定义类型)。

  • 文本框
  • 数字文本框
  • CheckBox
  • ComboBox
  • 日期时间编辑器
  • 时间间隔编辑器
  • 链接

每个 DetailItem 都充当其 Value 属性的编辑器。您可以初始化项目值并在终止时获取结果。

或者,您可以提供 DetailView.DataSource(一个数据对象),并将项的 DataMember 属性分配给 DataSource 对象合适属性的名称。然后,该项将成为该 DataSource 属性的编辑器。

我们上面描述的是单向数据绑定(目标到源)。但是,如果 DataSource 对象支持 INotifyPropertyChanged 接口,我们将获得双向绑定。例如,想象一下 DataSource 属性包含在后台运行的 Web 通信结果。双向绑定意味着当新数据到达时,DetailView 将自动刷新其内容。

DetailView 的其他方面

  • 项目可以分组:第一个 DetailView 图显示了两个分组的项目,但分组足够灵活,可以允许您进行任何想要的组合。
  • DetailView 支持验证和错误反馈(通过为项目标签着色,请参见下图)。
  • 也支持可空数据类型。

关于数据绑定的说明

这不是 Silverlight/WPF 所支持的全功能绑定。AdvancedList/DetailView 只支持简单的一级绑定,即 UI 元素与 DataSource 对象属性之间的直接关联。换句话说,不支持复杂的属性路径。(这可能是 .NET 程序员的期望。)

Resco MobileForms Toolkit

日历

Resco MobileForms ToolkitResco MobileForms Toolkit

该工具包包含两个日历控件——MonthCalendar 和 WeekCalendar。它们都设计用于展示不同的约会视图。在我们深入探讨这个可能对大多数读者来说过于具体的话题之前,让我们介绍一个具有普遍意义的应用。

左图展示了用作日期选择器的 MonthCalendar。日历单元格中的点表示约会,但这只是一个附加功能。如果您不提供约会,它们将不会显示,用户将获得纯日期选择。

回到约会。那么,约会是什么?

没什么复杂的,它是一个时间间隔。如您所见,MonthCalendar 只需要这个。它只为有约会的日期提供一个标准的标记(点)。

另一方面,WeekCalendar 更进一步,显示约会名称。它甚至允许选择绘制属性——颜色、字体等。
如果您想使用约会,您必须提供实现 IAppointmentDataSource 接口的数据源。该接口基于查询(查询给定时间间隔内的约会、查询约会名称、颜色等)。约会对象是完全不透明的(它们是 Object 实例),因此您可以以任何可想象的方式实现它们。

也许再补充一句,让那些不理解 WeekCalendar 含义的人了解一下。它显示 7 列,每周一天一列。列之间的边框在图中略微可见。WeekCalendar 支持 AppointmentClicked 事件。因此,您拥有构建(例如)简单计划应用程序所需的一切。

其他控件

Resco MobileForms ToolkitResco MobileForms Toolkit

日期/时间选择器控件允许您选择日期、时间(右图)或两者(左图)。您可以自定义文本按钮和时间增量。(即 +/- 按钮按下的对应值。)

请注意,MonthCalendar 提供了另一种日期选择器。

Resco MobileForms ToolkitResco MobileForms Toolkit

上图显示了两个选择器

  • ActionSelector(左图)接受一个字符串列表和一个 Action 回调,当用户进行选择时会执行该回调。这是一个方便的小工具,可以让您使用一个命令代替组合框和相关的切换逻辑。
  • ListSelector(右图)显示一个弹出控件,允许从提供的对象集合中进行选择。您可以从字符串列表或通用对象列表中进行选择。在后一种情况下,您可以设置 DisplayMember 属性指向要显示的对象的属性;否则,选择器将仅显示 object.ToString()。例如,上图通过 Name 属性表示对象。

最后是剩余的控件

  • ProgressBar(见下图)
  • TabBar:您已经在演示 DetailView 用法的屏幕截图中看到了这个控件。

Resco MobileForms Toolkit

第二部分 – 编码主/明细应用程序

让我们演示如何构建典型的主/明细应用程序。在接下来的内容中,我们将只关注主要主题。错误处理、布局相关等内容将省略。您可以从 http://www.resco.net/developer/mobilelighttoolkit 下载示例的完整代码。

Data Model

让我们从数据描述开始。我们的数据对象将是下面定义的 Entity 类型。

public enum DrinkType {
	EDrink_Water,
	EDrink_Juice,
	EDrink_Wine,
	EDrink_Beer
};

public class Entity {
	public string FirstName { get; set; }
	public string LastName { get; set; }
	public string Phone { get; set; }
	public string Email { get; set; }
	public DateTime Birth { get; set; }
	public bool Smoker { get; set; }
	public DrinkType Drinks { get; set; }

	public string FullName { get { return FirstName + " " + LastName; } }
	public override string ToString() { return FullName; }
}

接下来,这是执行所有数据相关操作的数据模型类。

public class Model  {
	public Model()  { /* Generate data... This part is omitted. */ }
	public IList Data { get;}
}

列表实现

public class MasterActivity : Activity
{
	// UI related - the initialization of these members is omitted
	private Bitmap _callIcon;
	private int _largeTextSize, _smallTextSize, _rowHeight, _iconSize;

	private Model _data;

	// List creation
	protected override void OnCreate(Bundle bundle)  {
		base.OnCreate(bundle);

		_data = new Model();
		var list = new AdvancedList(this) { Id=2, DataSource=_data.Data };

		// Setup templates for selected and unselected row states
		list.TemplateIndex = SetupDefaultTemplate(advancedList);
		list.SelectedTemplateIndex = SetupSelectedTemplate(advancedList);

		//set up layout...
	}
	
	//   UnselectedTemplate displays: [Full name]  [Smoker icon]
	private int SetupDefaultTemplate(AdvancedList list)	{
		int tIndex = advancedList.AddTemplate
			(null, -1, _rowHeight, 0);
		list.RowTemplates[tIndex].DefaultWidth = 320;

		// We rely on ToString() to supply displayed contents
		list.AddCell(tIndex, ListCellKind.Text, null, false, null,
			new int[] {5, 0, -1, _rowHeight},
			ListCellAnchor.None,
			new ListCellStyle() {FontSize=_largeTextSize, AutoHeight=true}
		);

		AddSmokerCell(list, tIndex);
		return tIndex;
	}

	//   SelectedTemplate: [Full name]  [Phone]  [Email]  [Details button]
	private int SetupSelectedTemplate(AdvancedList list)   {
		int tIndex = list.AddTemplate
			(null, -1, _rowHeight + 2*_smallTextSize, 0);
		list.RowTemplates[tIndex].DefaultWidth = 320;

		ListCellStyle largeStyle = new ListCellStyle()
			{ FontSize = _largeTextSize, AutoHeight = true };
		ListCellStyle smallStyle = new ListCellStyle()
			{ FontSize = _smallTextSize };


		list.AddCell(tIndex, ListCellKind.Text, null, false, "FullName",
			new int[] { 5, 0, 180, _rowHeight },
			ListCellAnchor.AllSides, largeStyle);
		list.AddCell(tIndex, ListCellKind.Text, null, false, "Phone",
			new int[] { 31, _rowHeight, 134, _smallTextSize },
			ListCellAnchor.Bottom, smallStyle);

		list.AddCell(tIndex, ListCellKind.Text, null, false, "Email"...);
		list.AddCell(tIndex, ListCellKind.Image, null, true, _callIcon ... );
		list.AddCell(tIndex, ListCellKind.Image, null, true, Resource.Drawable.email...);
		list.AddCell(tIndex, ListCellKind.Button, "Detail", true, null...);

		return tIndex;
	}
}

使用自定义单元格

如果您需要比现有单元格类型更多的功能,您可以使用自定义单元格。以下代码展示了一个具有图形内容的自定义单元格示例。SmokerCell 期望它绑定到一个 Boolean 属性,该属性指示是否绘制吸烟者图标。这是一个非常简单的示例,它只是覆盖了绘图。自定义单元格通常更复杂,例如当它们实现编辑操作时。

public class SmokerCell : Cell
{
	private Bitmap _image;

	public SmokerCell(Context context) : base(context)  {
		_image = BitmapFactory.DecodeResource
			(context.Resources, Resource.Drawable.cig);
	}

	public override void Draw
		(Canvas canvas, Rectangle parentBounds, object data, bool selected, Paint paint)
	{
		Rectangle bounds = this.Bounds;
		if (bounds.Width < 0)
			bounds.Width = parentBounds.Width - bounds.Left;

		if (data == null || this.CellSource == null)
			return;		//nothing to draw

		object objData = this.GetValue(data, this.CellSource.ColumnName);
		if ((bool)objData == true)  {
			canvas.Save();
			canvas.ClipRect(bounds.Left, bounds.Top, bounds.Right, bounds.Bottom);
			canvas.DrawBitmap(_image, bounds.Left, bounds.Top, paint);
			canvas.Restore();
		}
	}
}

我们仍然需要提供 AddSmokerCell() 方法,该方法已被 SetupDefaultTemplate() 使用。请注意,要添加自定义单元格,您需要直接操作模板的 Cells 集合。

public class MasterActivity  : Activity
{
	private void  AddSmokerCell(AdvancedList advancedList, int  tIndex)
	{
		var smokerCell = new  SmokerCell(this);
		smokerCell.CellSource.ColumnName  = "Smoker";
		smokerCell.Bounds  = 
		new Rectangle(280, (_rowHeight-_iconSize)/2,  _iconSize, _iconSize);
		advancedList.RowTemplates[tIndex].Cells.Add(smokerCell);
	}
}

向列表中添加过滤器

首先,我们必须使数据模型具备过滤数据的能力。

public enum FilterBy 
{
	FirstName,
	LastName
};

public class Model {
	//  Returns list of entities matching supplied filter
	public  IList FilteredData(string filterText, FilterBy filterBy) {
		List<Entity>  newList = new List<Entity>();
		Data.ForEach((e)  => { 
			string  name = (filterBy==FilterBy.FirstName) ? e.FirstName : e.LastName;
			if  (name.ToLower().StartsWith(filterText))
				newList.Add(e);
		});
		return  newList;
	}
}

接下来,我们将在 OnCreate() 中设置过滤器并提供 FilterChanged 事件的处理程序。

public class MasterActivity  : Activity
{
	protected  override void  OnCreate(Bundle bundle)  {
		//  …
		SetupFilter(list);
	}
	private void  SetupFilter(AdvancedList list)   {
		//  2 filters are available 
		var  filterGroup = new FilterGroup();
		filterGroup.Filters.Add(new  FilterItem("filter", "Filter", 
			new  List<KeyValuePair<string, object>>( new[] {
				new  KeyValuePair<string, object>
					("First  name", FilterBy.FirstName),
				new  KeyValuePair<string, object>
					("Last  name", FilterBy.LastName),
		})));
		filterGroup["filter"].SelectedIndex = 1;   // Set default search
		
		list.FilterGroup  = filterGroup;
		list.IsFilterVisible  = true;
		list.EmptyFilterText  = "Search";
		list.FilterChanged  += HandleListFilterChanged;
	}
	
	//  Handler of the FilterChanged event. 
	//  We ask the model for new DataSource with filtered data 
		private void  HandleListFilterChanged(object sender,  EventArgs e) {
			var list =  FindViewById(2) as AdvancedList;
			list.DataSource  = _data.FilteredData(
				list.FilterText,               // Text typed by the user 
				(FilterBy)list.FilterGroup[0].SelectedValue
		);
	}
}

与 DetailView 的协作

选定的模板已经有了 Details 按钮。我们需要为 ButtonClick 事件设置处理程序,并提供传递选定 Entity 的方式。

 
public class MasterActivity  : Activity
{
	// Used by DetailView to get selected Entity (data sharing)
	public static object SelectedEntity { get;  set; }
	protected  override void  OnCreate(Bundle bundle)  {
		// Setup handler for [Details] click.
		// It will launch DetailView presenting entity details.
		list.ButtonClick  += HandleListButtonClick;
	}
	void HandleListButtonClick(object  sender, ButtonClickEventArgs e)  {
		AdvancedList  list = FindViewById(2) as AdvancedList;
		MasterActivity.SelectedEntity = _data.Data[list.SelectedIndex];
		StartActivity(typeof(DetailActivity));
	}
}

添加 DetailView

我们的工作在概念上很简单:

  • 创建 DetailView 实例
  • 将其 DataSource 设置为 AdvancedList 中选定的实体
  • 选择我们想要编辑的实体属性,并将相应的 DetailItems 添加到 DetailView
  • 提供可选的项目分组
  • 设置布局(此部分省略)
public class DetailActivity  : Activity
{
	protected override void OnCreate(Bundle bundle)
	{
		base.OnCreate(bundle);
		DetailView  view = new DetailView(this);
 
		// All items have binding
		// We specify a) Item label through constructor, b)  DataMember for binding
		var textItem = new  DetailItemTextBox("First Name")
			{DataMember="FirstName"};
		view.AddItem(textItem);
 
		textItem = new DetailItemTextBox("Last Name")
			{DataMember="LastName"};
		view.AddItem(textItem);
 
		textItem = new DetailItemTextBox("Phone")
			{DataMember="Phone"};
		view.AddItem(textItem);
 
		textItem = new DetailItemTextBox("Email") 
			{DataMember="Email", Kind=DetailItemTextBox.TextKind.Email};
		view.AddItem(textItem);
 
		var dateItem = new DetailItemDateTime("Birth", null, DateTimePicker.Parts.Date)
			{DataMember="Birth"};
		view.AddItem(dateItem);
 
		var checkItem = new DetailItemCheckBox(this, "Smoker")
			{DataMember="Smoker"};
		view.AddItem(checkItem);
			
		// Take over entity selected in AdvancedList
		view.DataSource = MasterActivity.SelectedEntity;
			
		// The items will be grouped as follows:
		//             FirstName  + LastName
		//             Phone +  Email
		view.SetupGroups(2,  2);
		// Set up layout...
	}
}

向 DetailView 添加组合框

以下代码演示了如何从枚举中选择值。

 
public class DetailActivity  : Activity
{
	// Helper class that adds labels to the DrinkType values 
	private class ComboItem
	{
		public string Name { get; set; }
		public DrinkType Value { get;  set; }
	}
	
	// List of all possibilities - will be used as combo box  DataSource
	private static List<ComboItem> _comboSource = new List<ComboItem>()
	{
		new ComboItem() {Name="Water", Value=DrinkType.EDrink_Water},
		new ComboItem() {Name="Juice", Value=DrinkType.EDrink_Juice},
		new ComboItem() {Name="Wine", Value=DrinkType.EDrink_Wine},
		new ComboItem() {Name="Beer", Value=DrinkType.EDrink_Beer},
	};

	// SetupCombo() defines the contents of the combo box
	private void  SetupCombo(DetailItemComboBox cb)
	{
		cb.DisplayMember = "Name";      // Property to  be displayed
		cb.ValueMember = "Value";       // Property  to be selected
		cb.DataSource = _comboSource;   // List content
	}

	// Code to be added to OnCreate()
	protected override void OnCreate(Bundle bundle)
	{
		// ...
		var comboItem = new DetailItemComboBox("Drinks") 
			{DataMember="Drinks"};
		SetupCombo(comboItem);
		view.AddItem(comboItem);
	}
}

关于作者

Jan Slodicka。编程超过 30 年。涵盖了多个桌面平台和编程语言。自 2003 年以来,一直在 Resco 从事移动技术工作——Palm OS、Windows Mobile、Windows Phone 7、Android。

您可以通过 `jano at resco.net` 或通过 Resco 论坛联系我。

Resco MobileForms Toolkit - Android Edition 可以从 http://www.resco.net/developer/mobilelighttoolkit 下载。该工具包包含一组有用的控件,可简化 Mono for Android 编程。除了 Android,还有 Windows Mobile、Windows Phone 7 和 iOS 版本。
Resco 是一家拥有悠久移动编程传统的公司,涵盖许多平台,以及面向最终用户和开发者的应用程序工具。

© . All rights reserved.