The article presents Resco MobileForms Toolkit, Android Edition. Besides the brief characteristics of the included components special attention is devoted to the writing of LOB applications using master-detail concept.
Introduction
According to recent research of Gartner, the Android operating system should own 50% of mobile market share in 2012.
Android developers use primarily Java and Google-based Java libraries. While this sounds like a multi-platform strategy, the reality is a bit different:
- Android does not use established Java standards, i.e. Java SE and ME.
- Popularity of Java among the developers is decreasing significantly over time. Instead, the most popular programming language is with no doubts C (C, C++, C# and objective C).
On 6-April-2011 Novell announced the awaited release of their
Mono for Android. Mono is a tool that enables mobile programmers to use C# and Microsoft Visual Studio to create .NET based applications that run on Android phones and tablets.
This can save a lot of time and money since the developers can share common code for multiple platforms including Android, iOS (using MonoTouch), Windows Phone 7, Windows desktops and Windows server.
Resco MobileForms Toolkit
The toolkit has a long history. It was originally built for Windows Mobile platform where it offered a superior list of controls that helped thousands of (mainly corporate) developers to build mobile applications with attractive user interface.
This wasn't easy to achieve given very constrained platform support (.NET Compact Framework) that was built primarily for desktop programming.
The toolkit origins were inspired by LOB applications built on master-detail principle. This gave birth to AdvancedList and DetailView controls that were later followed by dozens of other, more specialized controls.
The toolkit became popular because it was built primarily for mobile development. However trivial it may sound today, it wasn't so a few years ago.
By using the toolkit the developers could capitalize on their .NET/C# experience gained on desktop platforms; they could relatively easily build mobile LOB applications without delving into complex platform details.
It is this group of Windows Mobile developers who will find the Android version of the toolkit very familiar and will be able to use it very fast. Those who never worked with Resco toolkit may find the inspiration by looking at the screen samples bellow.
Prerequisites
The target audience is a C# .NET programmer. It is good if the reader has basic understanding of Android programming, but - strictly speaking - this is not required.
IT decision makers may read the article (mainly its first part) to get a general idea what can be done with the toolkit.
The programmers, who want to actively test the Toolkit, need to have installed Mono for Android. (http://mono-android.net/Installation)
Part I - A Tour Over the Toolkit
Master/detail scenario
The above image presents master-detail scenario built with help of AdvancedList (left figure) and DetailView controls (right). We'll demonstrate the programming of both controls in the second part of the article. At this place let us introduce in general terms what can be done with them.
The
AdvancedList control displays list of objects, in this case customers. Each list row displays one customer object acc. to the template specified by the programmer. As can be seen in the figure the normal template shows customer name and address. Selected customer (the one you tap upon) uses another template with different colors and additional action buttons.
The programmer has to provide the list DataSource (any object collection), design data templates (select data properties to be displayed, their formatting etc.), write button handlers. In the case shown on the above images the handler for More button launches customer details editor, i.e. a DetailView instance.
Row template is a collection of cells. Each cell has position (bounding box within the row), style (fonts, colors etc.) and cell data. Here are typical cell examples:
- Button cell
- Text cell with constant (string) content
- Text cell displaying property of the data object (data binding)
- Text cell displaying object.ToString() value. (This happens when no property name was supplied.)
- Image cell with bitmap content
Another optional list feature is the filtering. The programmer has to write the filter logic that accepts the user text and returns modified item list. Multiple filters are supported.
How the filtering looks like from the user point of view? The user types the text into the search bar and selects the filter from the (one- or two-level) filter menu. The screenshots below demonstrate 2-level filtering.
DetailView control shows a list of items. Each item consists of label and data editor and can be formatted in a number of ways (fonts, colors, alignment…). You can select from seven predefined item editors (or provide your own custom type):
- TextBox
- Numeric TextBox
- CheckBox
- ComboBox
- DateTime editor
- TimeSpan editor
- Link
Every DetailItem functions as an editor of its Value property. You can initialize item values and pick up the results upon termination.
Or you can supply DetailView.DataSource (a data object) and assign item DataMember property to the name of suitable property of the DataSource object. Then the item becomes an editor of this DataSource property.
What we described above is one-way data binding (target-to-source). If, however, the DataSource object supports INotifyPropertyChanged interface, we get two-way binding. Imagine for example that the DataSource property contains results of a web communication running on background. Two-way binding implies that the DetailView will automatically refresh its content whenever new data arrives.
More aspects of the DetailView:
- Items can be grouped: The first DetailView figure shows always two items grouped together, but the grouping is flexible enough to allow any grouping you may wish.
- DetailView supports validation and error feedback (by coloring item labels, see the next figure).
- Nullable data types are supported as well.
Note about the
data binding:
This is not full-flexed binding as supported by Silverlight/WPF. AdvancedList/DetailView support only simple one-level binding, i.e. a direct relation between an UI element and a DataSource object property. In other words no complex property paths are supported. (This is probably what the .NET programmers expect.)
Calendars
The Toolkit contains two calendar controls - MonthCalendar and WeekCalendar. They both are designed to present different views for appointments. Before we delve into this topic that's probably too specific for most readers, let's present one application of general interest.
The left figure presents MonthCalendar serving as a date selector. The dots in the day cells show appointments, but that's add-on feature. If you don't supply appointments, they won't be shown and the user gets pure date selection.
Back to appointments. So what is it - an appointment?
Nothing complicated, it is a time interval. As you see, the MonthCalendar needs just that. It only gives some standard mark (the dot) to the days where you have an appointment.
On the other hand, the WeekCalendar goes to a greater depth by displaying appointment name. It even allows for selection of drawing attributes - colors, fonts etc.
If you want to use appointments, you have to supply DataSource that implements IAppointmentDataSource interface. The interface is based on queries (query for appointments within given time interval, query for appointment name, color etc.). The appointment objects are fully opaque (they are Object instances), hence you may implement them in any thinkable manner.
Perhaps one additional note for those not understanding the meaning of WeekCalendar. It displays 7 columns, one column per each week day. The column borders are slightly visible in the figure. The WeekCalendar supports AppointmentClicked event. Hence you have all you need to build (say) a simple planner application.
Other controls
Date/time picker control lets you select either the date, time (right figure) or both (left). You can customize the text buttons and the time increment. (I.e. the value corresponding to the presses of +/- button.)
Note that the MonthCalendar provides an alternative date selector.
Above pictures display two selectors:
- ActionSelector (left) takes a list of strings and an Action callback that is executed when the user makes the selection. Little handy tool that lets you use one command instead of a combo box and a related switch logic.
- ListSelector (right) presents a popup control that allows the selection from the supplied object collection. You may select from a list of strings or from a list of general objects. In the latter case you may set up the DisplayMember property to point to object property to be displayed; if not, the selector simply displays object.ToString(). For example the above screenshot represents objects by their Name property.
And finally the remaining controls:
- ProgressBar (see the next figure)
- TabBar: You already saw this control on the screenshot demonstrating the use of the DetailView.
Part II - Coding Master-Detail Application
Let's demonstrate how to build typical master-detail application. In the following we shall concentrate on the main topics only. Error handling, layout related things etc. will be omitted. The full code of the sample can be downloaded from
http://www.resco.net/developer/mobilelighttoolkit.
Data Model
Let's start with data description. Our data objects will be of type Entity as defined below:
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; } } |
As next, here is the data model class performing all data-related operations:
public class Model { public Model() { /* Generate data... This part is omitted. */ } public IList Data { get;} } |
List implementation
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; } } |
Using custom cells
In case you need more than the existing cell types offer, you can use custom cells. The following code shows an example of a custom cell with graphical content. SmokerCell expects that it is bound to a Boolean property indicating whether to draw the smoker icon. This is a really simple example that just overrides the drawing. Custom cells are often more complex, for example when they implement edit action.
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(); } } } |
We still have to supply AddSmokerCell() method that was already used by SetupDefaultTemplate(). Note that to add a custom cell you need to operate directly on the template Cells collection.
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); } } |
Adding filters to the list
At first we have to equip the data model with the ability to filter the data.
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; } } |
As next we'll setup the filter in OnCreate() and provide handler of the FilterChanged event:
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 ); } } |
Cooperation with the DetailView
Selected template has already the Details button. We need to set up the handler for the ButtonClick event plus provide the way how to pass selected 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)); } } |
Adding the DetailView
What we need to do is conceptually simple:
- Create DetailView instance
- Set up its DataSource to the entity selected in AdvancedList
- Select entity properties we want to edit and add corresponding DetailItems to the DetailView
- Provide optional item grouping
- Set up layout (this part is omitted)
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... } } |
Adding combo box to the DetailView
Following code demonstrates how to select a value from an enumeration.
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); } } |
Resco MobileForms Toolkit - Android Edition can be downloaded from
http://www.resco.net/developer/mobilelighttoolkit. The Toolkit contains a set of useful controls that simplify Mono for Android programming. Besides Android, there is also Windows Mobile, Windows Phone 7 and iOS edition.
Resco is a company with a long tradition of mobile programming covering many platforms and both end-user applications and developer tools.