Content
Introduction
There are already several articles on Codeproject implementing some kind of app which visualizes a members score and the latest articles and/or the community additions for all kind of platforms, like Android, iOS, Windows Phone, Windows, mostly going back to the mother of all CP Vanity style applications, even with different functionality. So, why yet another one? There is none using C# as a basis for the iOS, Android and Windows Phone version of the application and as such creating from a more or less (this is what I wanted to find out), single code base all three applications. So, this article is not so much about how to do things on the various platforms, but more about how to enable code-reuse between those platforms.
DISCLAIMER 1:
I chose Windows Phone 7 and not 8 for practical reasons: I don't have a Windows 8 machine available.
DISCLAIMER 2:
For the iOS and Android version you will need Xamarin. I wanted the source-code to be compilable with the free version of Xamarin and this also means accepting the constraints imposed by that choice:
- A maximum size for the application
- No use of native external libraries
- No use of some .NET features like WCF, System.Data.SqlClient
For a full list of differences, go to the Xamarin store
The above constraints have lead to me skipping:
- all error handling
- some needed functionality for a robust implementation, like the ability to cancel asynchronous tasks
As a result, this code is not really production ready and is more to be seen as a proof of concept. Still, I think it gives a good impression of what is possible using the Xamarin framework for mobile application development.
I assume a basic knowledge of the iOS, Android and Windows Phone 7 platforms. I will NOT discuss what for example a Segue
is on the iOS platform or an Activity
or Intent
on the Android platform.
The end result
On Youtube
If you can not wait to see what the result looks like, go watch these Youtube video's
By Screenhots
Following are some screenshots of the youtube video's, with the same screens side by side for the three platforms:
Memberlist
Member Details
Member Articles
Member Reputation
Switch To Articles
Articles
Article Categories
Community
Community Categories
The business code
Crawling web pages
Concepts
There is currently not a very extensive web service interface to the Codeproject so to get the data we need to rip it from the webpages. There is a first opportunity for code sharing here:
- The code for ripping the HTML pages is hopefully independent of the platform
- The project type hopefully is too
The first point is luckily possible: you can find all code for ripping a web page in the Ripit folder of the source code.
The second point is possible for the iOS and Android version by using a portable class library type project. Unfortunately it is not possible for Windows Phone 7. However if I had chosen Windows Phone 8 we would have been able to also use that type of project.
The Code
Although not really important for the discusion at hand I still want to explain a little bit about the idea behind the implementation.
While implementing my native iOS version of CPVanity, during the development of the part which crawls the codeproject website, I noticed implementing a lot of boilerplate code and also repeating myself. I missed the C# concept of attributes. So, in implementing the C# version I decided to do this the right way. Enter the Ripit library.
The Ripit library allows you to define the values to be used to fill an object. The web crawling is based on regular expressions. For this, it has a set of attributes allowing you to define the regular expression to use for capturing the value of a property and the URL of the web page used as the source for the regular expression.
The attributes are:
HttpSourceAttribute
: This attribute allows to define an url to get a web page containing a piece of data. You can define multiple of these attributes on the class whose objects you want to fill. It takes 2 arguments: an id to identify the attribute instance and the url to use. SourceRefAttribute
: This attribute should be applied on properties of a class and defines wich of the HttpSourceAttribute
on the class should be used as a source for the regular expression capturing the value for that property. The id supplied is the id of the HttpSourceAttribute
to use. PropertyCaptureAttribute
: This attribute should also be applied on properties of a class and is the actual attribute defining the regular expression to use to get the value for the property. It has 3 arguments: the regular expression itself, the index of the capture group and finally if it is optional or not. CollectionCaptureAttribute
: This attribute must be applied on the class and is meant to be used to fill collections. It has a single argument: the regular expression to be used to capture the fragments of text used to fill the values of the objects. Every fragment found is used to fill an object of the class. Thus, the fragments are not the values for the objects, but contain the text that must be further analized to fill the objects. DefaultValueAttribute
: if the PropertyCaptureAttribute
is optional and no value is found, then this attribute defines the value to be used.
The workhorse class is the ObjectBuilder
class. It provides methods to supply an object which will then be filled with data. The methods also have an asynchronous equivalent.
object Fill(object objectToFill, Dictionary<String, String> paramList, CancellationToken ct)
Task<object> FillAsync(object objectToFill, Dictionary<String, String> paramList, CancellationToken ct)
IList<T> FillList<T>(IList<T> listToFill, Dictionary<String, String> paramList, Func<T> itemFactory)
Task<IList<T>> FillListAsync<T>(IList<T> listToFill, Dictionary<String, String> paramList, Func<T> itemFactory, CancellationToken ct)
IList<RSSItem> FillFeed(IList<RSSItem> feedToFill, Dictionary<String, String> paramList, CancellationToken ct)
Task<IList<RSSItem>> FillFeedAsync(IList<RSSItem> feedToFill, Dictionary<String, String> paramList, CancellationToken ct)
The asynchronous methods use the System.Threading.Task
class which is available for use in Xamarin based projects. Windows Phone 7 has no support for the Task class. Fortunately, there is an open source library based on the Mono implementation, available here which is useable in Windows Phone 7 applications.
The ObjectBuilder
class reads the HttpSourceAttributes
defined on the type of the object supplied, downloads the pages and stores them in a dictionary with the Id
as key value. Next it iterates the properties of the class, gets SourceRefAttribute
s and PropertyCaptureAttribute
s which it uses to get the value for the property.
public object Fill(object objectToFill, Dictionary<String, String> paramList, CancellationToken ct)
{
Dictionary<int, string> globalSources = GetSources(objectToFill, paramList, ct);
if (ct == CancellationToken.None && globalSources == null)
return null;
return FillFromSources(objectToFill, globalSources, ct);
}
private Dictionary<int, string> GetSources(object objectToFill, Dictionary<String, String> paramList, CancellationToken ct)
{
Dictionary<int, string> urlSources = GetSourceUrls(objectToFill, paramList, ct);
Dictionary<int, string> globalSources = new Dictionary<int, string> ();
foreach (KeyValuePair<int, string> entry in urlSources) {
globalSources.Add (entry.Key, pageText);
}
return globalSources;
}
private Dictionary<int, string> GetSourceUrls(object objectToFill, Dictionary<String, String> paramList, CancellationToken ct)
{
Dictionary<int, string> urlSources = new Dictionary<int, string> ();
Type objectType = objectToFill.GetType();
object[] objectAttrs = objectType.GetCustomAttributes(false);
foreach (HttpSourceAttribute httpSource in objectAttrs.ToList().OfType<HttpSourceAttribute>()) {
urlSources.Add (httpSource.Id, mainUrl);
}
return urlSources;
}
private object FillFromSources(object objectToFill, Dictionary<int, string> globalSources, CancellationToken ct)
{
Type objectType = objectToFill.GetType();
foreach (PropertyInfo property in objectType.GetProperties ()) {
object[] propertyAttrs = property.GetCustomAttributes(false);
if (propertyAttrs.Length == 0)
continue;
List<Attribute> propertyAttrList = propertyAttrs.OfType<Attribute>().ToList();
SourceRefAttribute sourceRef = (SourceRefAttribute)propertyAttrList.OfType<SourceRefAttribute>().SingleOrDefault();
if (sourceRef == null || !globalSources.ContainsKey(sourceRef.SourceRefId)) {
throw new Exception ();
}
string sourceText = globalSources[sourceRef.SourceRefId];
bool foundValue = true;
foreach (Attribute textActionAttribute in propertyAttrList) {
if((textActionAttribute is PropertyCaptureAttribute) && foundValue)
{
PropertyCaptureAttribute capture = (PropertyCaptureAttribute)textActionAttribute;
Match match = Regex.Match(sourceText, capture.CaptureExpression, RegexOptions.IgnoreCase);
if (match.Success) {
string key = match.Groups [capture.Group].Value;
sourceText = key;
} else if (capture.IsOptional) {
foundValue = false;
} else {
throw new Exception ();
}
}
}
if (!foundValue) {
DefaultValueAttribute defaultValue = (DefaultValueAttribute)propertyAttrList.OfType<DefaultValueAttribute>().SingleOrDefault();
if (defaultValue != null) {
sourceText = defaultValue.Value;
}
}
if (property.PropertyType == typeof(string)) {
property.SetValue (objectToFill, sourceText, null);
}
else if (property.PropertyType == typeof(int)) {
int sourceAsInt = 0;
if (int.TryParse(sourceText, out sourceAsInt)) {
property.SetValue (objectToFill, sourceAsInt, null);
}
else {
throw new InvalidCastException();
}
}
else if (property.PropertyType == typeof(DateTime)) {
DateTime sourceAsDt = DateTime.Now;
if (DateTime.TryParse(sourceText, out sourceAsDt)) {
property.SetValue (objectToFill, sourceAsDt, null);
}
else {
throw new InvalidCastException();
}
}
}
return objectToFill;
}
For filling collections an extra argument must be supplied providing a factory method used to create instances of the class with which to fill the collection.
public IList<T> FillList<T>(IList<T> listToFill, Dictionary<String, String> paramList, Func<T> itemFactory, CancellationToken ct) where T: class
{
Dictionary<int, string> globalSources = GetSources(listToFill, paramList, ct);
Type objectType = listToFill.GetType();
object[] objectAttrs = objectType.GetCustomAttributes (false);
CollectionCaptureAttribute captureAttribute = objectAttrs.OfType<CollectionCaptureAttribute> ().SingleOrDefault ();
MatchCollection matches = Regex.Matches(globalSources[0], captureAttribute.CaptureExpression, RegexOptions.IgnoreCase);
foreach (Match match in matches) {
Dictionary<int, string> targetSources = new Dictionary<int, string>();
targetSources.Add (0, match.Groups [0].Value);
T objectToFill = itemFactory ();
T filledObject = (T)FillFromSources(objectToFill, targetSources, ct);
listToFill.Add (filledObject);
}
return listToFill;
}
The code for analysis of the RSS feeds is based on this SO question.
The Codeproject website
Concepts
We want following pieces of functionality on all three platforms:
- Get data on our profile and of people we are interested in.
- Maintain a list of people we are interested in.
- Get a list of the latest articles published, preferably selectable by category
- Get a list of the latest discussions, also preferably selectable by category.
Again, because there are no platform specific things here, we want all this in a common library.
Unfortunately this is not entirely possible.
The classes representing the members, their articles, etc... are common to the three applications. They are simple Plain-Old-C#-Object-style classes and are put inside a Portable Class Library shareable in the Xamarin projects. The Windows Phone 7 version has its own library including the same files simply because it dioesn't support Portable Class Libraries. However, conceptyally it would be possible. There is however a small caveat: to make this portable I'm saving the member's avatar, which is actually an image, as a byte[]
type property containing the raw image data. Only when we need to display it do we convert it to a platform specific representation.
For saving the list of people and some of their data we need some kind of storage. And although the code itself is reusable for the iOS and Android platform, the underlying implementation is not. Database access is done through Mono.Data.Sqlite
which is not available in the Portable Class Library. And the Windows Phone 7 platform doesn't even have native Sqlite support, although an open source library is available.
Getting the avatars of the members is mostly generic, but the conversion to an image object for displaying in the app is platform specific as already mentioned in the discussion of the classes representing the members.
Storing the avatar on the phones storage is generic in Xamarin for Android and iOS, but completely incompatible with Windows Phone which uses the concept of isolated storage.
We solve all these discrepancies by using following features:
- For common code which gets compiled in platform specific code we use the file referencing feature of Xamarin and Visual Studio.
- For common code which eventually branches in platform specific code we use partial classes.
- For completely different implementations, we define a common interface but separate implementations.
Option 1 is used for the database code in the iOS and Android versions and for storing the member avatars on the file system. Option 3 is used for storing the avatars. An option I didn't use in this application is using compile time constants and the #define
feature.
The Code
As mentioned above, the POCO classes for representing the Codeproject website are shared in a Portable Class Library for Xamarin, and a plain library for Windows Phone 7.
On the file system we have a single folder with 2 library projects:
In the Xamarin IDE we have a single project referenced by the two platform specific projects
In the Visual Studio IDE we have a regular library project:
As an example of the type of classes in this project, following is the code of the CodeProjectMember
class:
namespace be.trojkasoftware.portableCPVanity
{
[HttpSource(1, "http://www.codeproject.com/script/Articles/MemberArticles.aspx?amid=Id")]
[HttpSource(2, "http://www.codeproject.com/script/Membership/view.aspx?mid=Id")]
public class CodeProjectMember
{
public int Id {
get;
set;
}
[SourceRef(1)]
[PropertyCapture(@"rticles by ([^\(]*)\(", 1, false)]
public string Name {
get;
set;
}
public byte[] Avatar {
get;
set;
}
}
}
Data access code has single code base for Xamarin but is included through file referencing in the iOS and Android projects. The Windows Phone 7 project has a totally different implementation.
On the file system we have a single folder with no project files but only source files. In the Xamarin IDE the files have references in their respective projects:
The actual implementation is in the monoCPVantity/Data/CodeProjectDatabase.cs file
The Windows Phone 7 app has its own specific implementation, resulting in separate files on the file system and the files included in the Visual Studio project:
I must make a side-note here: if you look at the Xamarin Tasky application, you will notice there is code sharing there for the database access code. To accomplish this however, they do not use the native SQLite support of the iOS and Android platform, but instead reimplement the Sqlite classes, using the .NET version as a basis. I choose not to do that because of the size restrictions of the app's in the free version of Xamarin Studio.
Similarly, the file storing code for the avatars is shared on the Xamarin platform but completely different from the Windows Phone 7 implementation.
You can find the Xamarin implementation in the monoCPVantity/Util/FileStorageService.cs file
The App
Displaying values and executing commands
Concepts
The iOS platform relies on the MVC pattern which is baked into it. Most C# developers are more familiar however with the MVVM pattern which relies heavily on data binding, something not supported by iOS. Android has it's own what-shall-we-call-it pattern and doesn't support data binding neither.
However, let's step back
What exactly are we trying to do when using the MVVM pattern? Part of it is about displaying values and binding controls to those values. For this we create a View Model which exposes properties to which we can bind our View. The binding part is a capability of WPF, but nothing is keeping us from creating View Models exposing properties for display-values and write the code for the "binding" ourselves.
A second part is about executing actions when certain events happen. Again, in a WPF application, a lot of this is possible through the magic of binding, but nothing is keeping us from going the old fashioned way and execute actions through event handling.
This is the approach taken in this application: I created View Models which are shared by all three apps through linking of the files in the project. For properties in the iOS and Android version we use simple assignment in the platform specific code. In the Windows Phone version I use data binding through the IPropertyChanged
interface.
The approach taken here is also a direct result of the choice to make the app's compilable in free available versions of the supported platforms. There are MVVM frameworks available that would enable more code sharing, but those bloat the size of the iOS and Android application past the limit set by Xamarin. I know, because it has happened to me.
The Code
Again, as with sharing of the database and file saving code, on the file system we have a single folder with no project files but only source files. In the Xamarin IDE the files have been referenced in their respective projects. This time however the files are also referenced in the Visual Studio project:
As an example of how it works, here's some code of the viewmodel for showing a Codeproject members profile. As you can see the loading of the member data is generic, and once it is loaded we call a delegate to be implemented in the UI for updating the UI.
namespace be.trojkasoftware.portableCPVanity.ViewModels
{
public delegate void MemberLoaded();
public partial class CodeProjectMemberProfileViewModel
{
public MemberLoaded MemberLoaded;
public int MemberId {
get;
set;
}
public void LoadMember(TaskScheduler uiContext) {
Dictionary<String, String> param = new Dictionary<string, string> ();
param.Add ("Id", MemberId.ToString());
Member = new CodeProjectMember ();
Member.Id = MemberId;
ObjectBuilder objectBuilder = new ObjectBuilder ();
Task<object> fillMemberTask = objectBuilder.FillAsync (Member, param, CancellationToken.None);
fillMemberTask.Start ();
fillMemberTask
.ContinueWith (x => LoadAvatar ())
.ContinueWith (x => MemberLoaded (), uiContext);
}
CodeProjectMember LoadAvatar() {
Member.Avatar = avatar;
return Member;
}
}
}
Notice how we implement it as a partial class. We will use this later in the Windows Phone 7 implementation.
Using this class in the Android app is done like this:
namespace be.trojkasoftware.droidCPVanity
{
[Activity (Label = "CPVanity", ParentActivity = typeof(MainActivity))]
[IntentFilter(new[]{Intent.ActionSearch})]
[MetaData(("android.app.searchable"), Resource = "@xml/searchable")]
public class CodeProjectMemberProfileActivity : Activity
{
protected override void OnCreate (Bundle bundle)
{
base.OnCreate (bundle);
ActionBar.SetDisplayHomeAsUpEnabled (true);
SetContentView (Resource.Layout.CodeProjectMemberProfileLayout);
memberName = this.FindViewById<TextView>(Resource.Id.textViewMemberName);
memberName.Text = "";
memberReputation = this.FindViewById<TextView>(Resource.Id.textViewMemberReputation);
memberReputation.Text = "";
memberIcon = this.FindViewById<ImageView> (Resource.Id.imageViewMemberImage);
memberIcon.SetImageBitmap (null);
spinner = this.FindViewById<ProgressBar>(Resource.Id.progressBar1);
spinner.Visibility = ViewStates.Gone;
viewModel = new CodeProjectMemberProfileViewModel ();
viewModel.MemberLoaded += this.MemberLoaded;
HandleIntent(Intent);
}
protected override void OnNewIntent(Intent intent)
{
Intent = intent;
HandleIntent(intent);
}
private void HandleIntent(Intent intent)
{
if (Intent.ActionSearch == intent.Action) {
String query = intent.GetStringExtra (SearchManager.Query);
viewModel.MemberId = int.Parse (query);
} else {
viewModel.MemberId = intent.Extras.GetInt (MemberIdKey);
}
spinner.Visibility = ViewStates.Visible;
var context = TaskScheduler.FromCurrentSynchronizationContext();
viewModel.LoadMember(context);
}
public override bool OnOptionsItemSelected (IMenuItem item)
{
switch (item.ItemId) {
case Resource.Id.action_member_add:
SaveCurrentMember ();
return true;
default:
return base.OnOptionsItemSelected(item);
}
}
private void SaveCurrentMember()
{
viewModel.SaveMember ();
}
void MemberLoaded() {
spinner.Visibility = ViewStates.Gone;
FillScreen ();
}
private void FillScreen()
{
memberName.Text = viewModel.Member.Name;
memberReputation.Text = viewModel.Member.Reputation;
TextView memberArticleCnt = this.FindViewById<TextView>(Resource.Id.textViewArticleCnt);
memberArticleCnt.Text = "Articles: " + viewModel.Member.ArticleCount;
TextView avgArticleRating = this.FindViewById<TextView>(Resource.Id.textViewArticleRating);
avgArticleRating.Text = "Average article rating: " + viewModel.Member.AverageArticleRating;
TextView memberBlogCnt = this.FindViewById<TextView>(Resource.Id.textViewBlogCnt);
memberBlogCnt.Text = "Blogs: " + viewModel.Member.BlogCount;
TextView avgBlogRating = this.FindViewById<TextView>(Resource.Id.textViewBlogRating);
avgBlogRating.Text = "Average blog rating: " + viewModel.Member.AverageBlogRating;
if (viewModel.Member.Avatar != null) {
Bitmap bitmap = BitmapFactory.DecodeByteArray (viewModel.Member.Avatar, 0, viewModel.Member.Avatar.Length);
memberIcon.SetImageBitmap (bitmap);
}
}
public static string MemberIdKey = "CodeProjectMemberId";
public static string MemberReputationGraphKey = "CodeProjectMemberReputationGraph";
TextView memberName;
TextView memberReputation;
ImageView memberIcon;
ProgressBar spinner;
CodeProjectMemberProfileViewModel viewModel;
}
}
Using this class in the iOS app is done like this:
namespace touchCPVanity
{
public partial class CodeProjectMemberProfileViewController : UIViewController
{
public CodeProjectMemberProfileViewController (IntPtr handle) : base (handle)
{
viewModel = new CodeProjectMemberProfileViewModel ();
viewModel.MemberLoaded += this.MemberLoaded;
}
public void SetMemberId(int memberId)
{
viewModel.MemberId = memberId;
}
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
this.SaveBtn.TouchUpInside += HandleTouchUpInside;
progressView = new UIActivityIndicatorView(UIActivityIndicatorViewStyle.Gray);
progressView.Center = new PointF (this.View.Frame.Width / 2, this.View.Frame.Height / 2);
this.View.AddSubview (progressView);
progressView.StartAnimating ();
var context = TaskScheduler.FromCurrentSynchronizationContext();
viewModel.LoadMember(context);
}
void HandleTouchUpInside (object sender, EventArgs ea)
{
viewModel.SaveMember ();
}
void MemberLoaded() {
progressView.StopAnimating ();
FillScreen ();
}
void FillScreen() {
this.MemberNameLbl.Text = viewModel.Member.Name;
this.MemberReputationLbl.Text = viewModel.Member.Reputation;
this.ArticleCountLbl.Text = "Articles: " + viewModel.Member.ArticleCount;
this.AvgArticleRatingLbl.Text = "Average article rating: " + viewModel.Member.AverageArticleRating;
this.BlogCountLbl.Text = "Blogs: " + viewModel.Member.BlogCount;
this.AvgBlogRatingLbl.Text = "Average blog rating: " + viewModel.Member.AverageBlogRating;
if (viewModel.Member.Avatar != null) {
NSData data = NSData.FromArray (viewModel.Member.Avatar);
this.MemberImage.Image = UIImage.LoadFromData (data, 1);
}
}
UIActivityIndicatorView progressView;
CodeProjectMemberProfileViewModel viewModel;
}
}
Finally using this class in the Windows Phone 7 app is done by extending it through a partial class extension:
namespace be.trojkasoftware.portableCPVanity.ViewModels
{
public partial class CodeProjectMemberProfileViewModel : CodeprojectBaseViewModel
{
public void Load()
{
Name = "Profile";
this.SaveMemberCommand = new ButtonCommandBinding<CodeProjectMember>(this.SaveMember);
}
private string memberName;
public string MemberName
{
get { return memberName; }
set { SetField(ref memberName, value, "MemberName"); }
}
public ButtonCommandBinding<CodeProjectMember> SaveMemberCommand { get; private set; }
public void SaveMember(CodeProjectMember member)
{
SaveMember();
}
public DataTemplate ItemDataTemplate
{
get
{
return App.Current.Resources["MemberProfileTemplate"] as DataTemplate;
}
}
public void OnMemberLoaded()
{
IsLoading = false;
MemberName = Member.Name;
MemberReputation = Member.Reputation;
MemberArticleCount = "Articles: " + Member.ArticleCount;
MemberAvgArticleRating = "Average article rating: " + Member.AverageArticleRating;
MemberBlogCount = "Blogs: " + Member.BlogCount;
MemberAvgBlogRating = "Average blog rating: " + Member.AverageBlogRating;
BitmapImage bitmapImage = new BitmapImage();
MemoryStream ms = new MemoryStream(Member.Avatar);
bitmapImage.SetSource(ms);
MemberAvatarImage = bitmapImage;
}
public override void OnLoad()
{
LoadMember(TaskScheduler.FromCurrentSynchronizationContext());
IsLoading = true;
}
}
}
Navigating the app
Concepts
The navigation concepts for all three platforms are very different.
The iOS platform works through the concepts of Segue
s and SDK provided methods to initiate navigation. In the sample app we use Segues exclusively. If you want navigation through code then have a look at this article. When using Segues, there is a method PrepareForSegue
called in your viewcontroller in which you can prepare the viewcontroller you are navigating to before it is being displayed. Passing parameters is done by defining properties on the target controller and setting them in the mentioned method. Return values are returned through delegates implemented by the source of the navigation and called by the target of the navigation.
The Android platform uses Intent
s to model navigation. You basically create an Intent
on which you set the Activity
you want to navigate to. Passing parameters is done by packing them into the Intent
. This Intent
is also forwarded to the target Activity
where you can then unpack the parameters.
Finally, the Windows Phone platform uses still another method: a first possibility is to use query strings. A second possibility is to use the navigation events on the pages. This one is somewhat similar to the iOS segue method. The different possibilities are described in this article Different ways of passing values betweenWinodws Phone 7 pages available on the internet.
From the above discussion you can see that there is not really a single way of navigation from one screen/page to another. That is why all navigation code is implemented in the platform specific projects. It should be possible however to create an abstraction of passing parameters between screens/pages, but due to the limitations of the free Xamarin platform I didn't invest any time in it.
The Code
iOS uses the concept of Segues. They are defined in the Storyboard which defines the navigation model of the application. By using the void PrepareForSegue (UIStoryboardSegue segue, NSObject sender)
method on a UIViewController
you will be notified when the user navigates to another screen and given the opportunity to set properties on the target view controller
namespace touchCPVanity
{
public partial class CodeProjectMemberProfileViewController : UIViewController
{
public override void PrepareForSegue (UIStoryboardSegue segue, NSObject sender)
{
base.PrepareForSegue (segue, sender);
var memberArticlesController = segue.DestinationViewController as CodeProjectMemberArticlesViewController;
if (memberArticlesController != null) {
memberArticlesController.SetMember(viewModel.Member);
}
}
}
}
namespace touchCPVanity
{
public partial class CodeProjectMemberArticlesViewController : UIViewController
{
public CodeProjectMemberArticlesViewController (IntPtr handle) : base (handle)
{
viewModel = new CodeProjectMemberArticlesViewModel ();
viewModel.ArticlesLoaded += this.ArticlesLoaded;
}
public void SetMember(CodeProjectMember member) {
viewModel.MemberId = member.Id;
}
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
progressView = new UIActivityIndicatorView(UIActivityIndicatorViewStyle.Gray);
progressView.Center = new PointF (this.View.Frame.Width / 2, this.View.Frame.Height / 2);
this.View.AddSubview (progressView);
progressView.StartAnimating ();
var context = TaskScheduler.FromCurrentSynchronizationContext();
viewModel.LoadMemberArticles (context);
}
}
}
Android used the concept of an Activity and Intents to navigate between them.
namespace be.trojkasoftware.droidCPVanity
{
[Activity (Label = "CPVanity", ParentActivity = typeof(MainActivity))]
[IntentFilter(new[]{Intent.ActionSearch})]
[MetaData(("android.app.searchable"), Resource = "@xml/searchable")]
public class CodeProjectMemberProfileActivity : Activity
{
public override bool OnOptionsItemSelected (IMenuItem item)
{
switch (item.ItemId) {
case Resource.Id.action_member_add:
SaveCurrentMember ();
return true;
case Resource.Id.action_member_articles:
GotoMemberArticles ();
return true;
default:
return base.OnOptionsItemSelected(item);
}
}
private void GotoMemberArticles()
{
var intent = new Intent (this, typeof(CodeProjectMemberArticlesActivity));
Bundle bundle = new Bundle ();
bundle.PutInt (CodeProjectMemberProfileActivity.MemberIdKey, viewModel.Member.Id);
bundle.PutString (CodeProjectMemberProfileActivity.MemberReputationGraphKey, viewModel.Member.ReputationGraph);
intent.PutExtras(bundle);
StartActivity (intent);
}
}
}
namespace be.trojkasoftware.droidCPVanity
{
[Activity (Label = "CPVanity")]
public class CodeProjectMemberArticlesActivity : Activity
{
protected override void OnCreate (Bundle bundle)
{
base.OnCreate (bundle);
MemberId = Intent.Extras.GetInt (CodeProjectMemberProfileActivity.MemberIdKey);
MemberReputationGraph = Intent.Extras.GetString (CodeProjectMemberProfileActivity.MemberReputationGraphKey);
viewModel.MemberId = MemberId;
}
}
}
Windows Phone uses the concept of query strings borrowed from navigating the web in combination with events.
namespace be.trojkasoftware.wpCPVanity
{
public partial class CodeprojectMemberPage : PhoneApplicationPage
{
private void GotoPage(string page)
{
NavigationService.Navigate(new Uri(page, UriKind.Relative));
}
}
}
namespace be.trojkasoftware.wpCPVanity
{
public partial class CodeprojectMemberProfilePage : PhoneApplicationPage
{
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
base.OnNavigatedTo(e);
String id = NavigationContext.QueryString["id"];
viewModel.MemberId = int.Parse(id);
}
}
}
The Xamarin platform looks really promising for code reuse, especially for applications with lots of business code. The current version even has some new features for sharing more code:
Unfortunately, the last feature is not available in the free version.
Following table shows the sizes of the various projects in this application: