使用 .NET 开发 Android 应用程序





0/5 (0投票)
本文介绍了 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)
第一部分 - 工具包概览
主/明细场景
上图展示了借助 AdvancedList(左图)和 DetailView 控件(右图)构建的主/明细场景。我们将在文章的第二部分演示这两个控件的编程。在此,让我们笼统地介绍一下它们的功能。
AdvancedList 控件显示对象列表,在本例中是客户。每个列表行根据程序员指定的模板显示一个客户对象。如上图所示,普通模板显示客户姓名和地址。选定的客户(您点击的客户)使用另一个具有不同颜色和附加操作按钮的模板。
程序员必须提供列表的数据源(任何对象集合)、设计数据模板(选择要显示的数据属性、它们的格式等)、编写按钮处理程序。在上图所示的例子中,“更多”按钮的处理程序会启动客户详细信息编辑器,即 DetailView 实例。
行模板是单元格的集合。每个单元格都有位置(行内的边界框)、样式(字体、颜色等)和单元格数据。以下是典型的单元格示例:
- 按钮单元格
- 具有常量(字符串)内容的文本单元格
- 显示数据对象属性的文本单元格(数据绑定)
- 显示
object.ToString()
值的文本单元格。(当未提供属性名时发生此情况。) - 具有位图内容的图像单元格
另一个可选的列表功能是过滤。程序员必须编写过滤逻辑,该逻辑接受用户文本并返回修改后的项目列表。支持多个过滤器。
从用户的角度来看,过滤是什么样的?用户在搜索栏中输入文本,然后从(单级或两级)过滤器菜单中选择过滤器。下面的屏幕截图演示了 2 级过滤。
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 程序员的期望。)
日历
该工具包包含两个日历控件——MonthCalendar 和 WeekCalendar。它们都设计用于展示不同的约会视图。在我们深入探讨这个可能对大多数读者来说过于具体的话题之前,让我们介绍一个具有普遍意义的应用。
左图展示了用作日期选择器的 MonthCalendar。日历单元格中的点表示约会,但这只是一个附加功能。如果您不提供约会,它们将不会显示,用户将获得纯日期选择。
回到约会。那么,约会是什么?
没什么复杂的,它是一个时间间隔。如您所见,MonthCalendar 只需要这个。它只为有约会的日期提供一个标准的标记(点)。
另一方面,WeekCalendar 更进一步,显示约会名称。它甚至允许选择绘制属性——颜色、字体等。
如果您想使用约会,您必须提供实现 IAppointmentDataSource
接口的数据源。该接口基于查询(查询给定时间间隔内的约会、查询约会名称、颜色等)。约会对象是完全不透明的(它们是 Object 实例),因此您可以以任何可想象的方式实现它们。
也许再补充一句,让那些不理解 WeekCalendar 含义的人了解一下。它显示 7 列,每周一天一列。列之间的边框在图中略微可见。WeekCalendar 支持 AppointmentClicked
事件。因此,您拥有构建(例如)简单计划应用程序所需的一切。
其他控件
日期/时间选择器控件允许您选择日期、时间(右图)或两者(左图)。您可以自定义文本按钮和时间增量。(即 +/- 按钮按下的对应值。)
请注意,MonthCalendar 提供了另一种日期选择器。
上图显示了两个选择器
- ActionSelector(左图)接受一个字符串列表和一个 Action 回调,当用户进行选择时会执行该回调。这是一个方便的小工具,可以让您使用一个命令代替组合框和相关的切换逻辑。
- ListSelector(右图)显示一个弹出控件,允许从提供的对象集合中进行选择。您可以从字符串列表或通用对象列表中进行选择。在后一种情况下,您可以设置 DisplayMember 属性指向要显示的对象的属性;否则,选择器将仅显示 object.ToString()。例如,上图通过 Name 属性表示对象。
最后是剩余的控件
- ProgressBar(见下图)
- TabBar:您已经在演示 DetailView 用法的屏幕截图中看到了这个控件。
第二部分 – 编码主/明细应用程序
让我们演示如何构建典型的主/明细应用程序。在接下来的内容中,我们将只关注主要主题。错误处理、布局相关等内容将省略。您可以从 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 是一家拥有悠久移动编程传统的公司,涵盖许多平台,以及面向最终用户和开发者的应用程序工具。