Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Windows-Phone-7

monoCPVanity: CP Vanity with Xamarin

4.94/5 (14 votes)
9 Feb 2015CPOL14 min read 36.4K   259  
3 times CP Vantiy from a single codebase (hopefully)

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

Image 1Image 2Image 3

Member Details

Image 4Image 5Image 6

Member Articles

Image 7Image 8Image 9

Member Reputation

Image 10Image 11Image 12

Switch To Articles

Image 13Image 14Image 15

Articles

Image 16Image 17Image 18

Article Categories

Image 19Image 20Image 21

Community

Image 22Image 23Image 24

Community Categories

Image 25Image 26Image 27

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:

  1. The code for ripping the HTML pages is hopefully independent of the platform
  2. 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 SourceRefAttributes and PropertyCaptureAttributes which it uses to get the value for the property.

C#
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);
}

// Download the pages and save them in a dictionary
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) {

        // get the page text and add it to the dictionary
        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>()) {

        // Get the URL from the attribute, resolve any parameters using the paramList
        //	and add if the urlSources dictionary
        urlSources.Add (httpSource.Id, mainUrl);
    }

    return urlSources;
}

// Fill the object from the sources
private object FillFromSources(object objectToFill, Dictionary<int, string> globalSources, CancellationToken ct)
{
    Type objectType = objectToFill.GetType();
    
    // We can only fill properties
    foreach (PropertyInfo property in objectType.GetProperties ()) {

        object[] propertyAttrs = property.GetCustomAttributes(false);
        if (propertyAttrs.Length == 0)
            continue;

        List<Attribute> propertyAttrList = propertyAttrs.OfType<Attribute>().ToList();

        // Get the attributes defining which source to use
        SourceRefAttribute sourceRef = (SourceRefAttribute)propertyAttrList.OfType<SourceRefAttribute>().SingleOrDefault();
        if (sourceRef == null || !globalSources.ContainsKey(sourceRef.SourceRefId)) {
            throw new Exception ();
        }

        // Get the attributes defining what the value to capture
        string sourceText = globalSources[sourceRef.SourceRefId];
        bool foundValue = true;
        foreach (Attribute textActionAttribute in propertyAttrList) {
        
            // Capture he value
            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 ();
                }
            }
        }
        
        // Apply any defaults if necessary
        if (!foundValue) {
            DefaultValueAttribute defaultValue = (DefaultValueAttribute)propertyAttrList.OfType<DefaultValueAttribute>().SingleOrDefault();
            if (defaultValue != null) {
                sourceText = defaultValue.Value;
            }
        }

        // Apply type conversions
        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.

C#
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);

        // Create a new object with ouot factory method
        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:

  1. Get data on our profile and of people we are interested in.
  2. Maintain a list of people we are interested in.
  3. Get a list of the latest articles published, preferably selectable by category
  4. 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:

  1. For common code which gets compiled in platform specific code we use the file referencing feature of Xamarin and Visual Studio.
  2. For common code which eventually branches in platform specific code we use partial classes.
  3. 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:

Image 28

In the Xamarin IDE we have a single project referenced by the two platform specific projects

Image 29

In the Visual Studio IDE we have a regular library project:

Image 30

As an example of the type of classes in this project, following is the code of the CodeProjectMember class:

C#
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)]
        // Articles by Serge Desmedt (Articles: 6, Technical Blogs: 2)
        [PropertyCapture(@"rticles by ([^\(]*)\(", 1, false)]
        public string Name {
            get;
            set;
        }

        // More similar definitions of other properties

        // For the avatar, we use a byte[]
        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:

Image 31

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:

Image 32

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:

Image 33

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.

C#
namespace be.trojkasoftware.portableCPVanity.ViewModels
{
    // provide a delegate so we can callback from the viewmodel into the platform specific code
    public delegate void MemberLoaded();

    public partial class CodeProjectMemberProfileViewModel
    {
        public MemberLoaded MemberLoaded;

        public int MemberId {
            get;
            set;
        }

        // More code here

        // Actions to be performed are exposed as methods on the viewmodel class
        public void LoadMember(TaskScheduler uiContext) {
            Dictionary<String, String> param = new Dictionary<string, string> ();
            param.Add ("Id", MemberId.ToString());

            Member = new CodeProjectMember ();
            Member.Id = MemberId;

            // We start the task of getting the members data
            ObjectBuilder objectBuilder = new ObjectBuilder ();
            Task<object> fillMemberTask = objectBuilder.FillAsync (Member, param, CancellationToken.None);

            // And when we are finished call the MemberLoaded() delegate on the UI thread
            fillMemberTask.Start ();
            fillMemberTask
                .ContinueWith (x => LoadAvatar ())
                .ContinueWith (x => MemberLoaded (), uiContext);
        }

        CodeProjectMember LoadAvatar() {

            // More code to get the members avatar
            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:

C#
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)
        {
            // Android specific stuff
            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;

            // In Android's OnCreate method, we instantiate the viewmodel and attach the delegate
            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);
            }

            // Initialize the UI for loading the members data, like providing a progress bar
            spinner.Visibility = ViewStates.Visible;

            // Start loading the member's data
            var context = TaskScheduler.FromCurrentSynchronizationContext();

            viewModel.LoadMember(context);
        }

        // On the Android platform we use Androids ability to create an optionsmenu
        //	and handle commands from it
        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 ();
        }

        // The delegate implementation stops the progressbars and 
        //	fills the UI controls with the members data
        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:

C#
namespace touchCPVanity
{
    public partial class CodeProjectMemberProfileViewController : UIViewController
    {
        public CodeProjectMemberProfileViewController (IntPtr handle) : base (handle)
        {
            // In the constructor, we instantiate the viewmodel and attach the delegate
            viewModel = new CodeProjectMemberProfileViewModel ();

            viewModel.MemberLoaded += this.MemberLoaded;
        }

        // Filling the viewmodel is doen using regular methods
        public void SetMemberId(int memberId) 
        {
            viewModel.MemberId = memberId;
        }

        public override void ViewDidLoad ()
        {
            base.ViewDidLoad ();

            // Attach any eventhandlers
            this.SaveBtn.TouchUpInside += HandleTouchUpInside;

            // Initialize the UI for loading the members data, like providing a progress bar
            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 ();

            // Start loading the member's data
            var context = TaskScheduler.FromCurrentSynchronizationContext();

            viewModel.LoadMember(context);
        }

        // iOS uses buttons to invoke commands
        void HandleTouchUpInside (object sender, EventArgs ea) 
        {
            viewModel.SaveMember ();
        }

        // The delegate implementation stops the progressbars and 
        //	fills the UI controls with the members data
        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:

C#
namespace be.trojkasoftware.portableCPVanity.ViewModels
{
    // the CodeprojectBaseViewModel base class just implements the INotifyPropertyChanged functionality
    public partial class CodeProjectMemberProfileViewModel : CodeprojectBaseViewModel
    {

        public void Load()
        {
            Name = "Profile";
            this.SaveMemberCommand = new ButtonCommandBinding<CodeProjectMember>(this.SaveMember);
        }

        // Here, we use the WPF binding capabilities
        private string memberName;
        public string MemberName
        {
            get { return memberName; }
            set { SetField(ref memberName, value, "MemberName"); }
        }

        // Command execution is done through CommandBinding
        // see http://www.mindfiresolutions.com/Binding-Button-Click-Command-with-ViewModal-2193.php
        public ButtonCommandBinding<CodeProjectMember> SaveMemberCommand { get; private set; }

        public void SaveMember(CodeProjectMember member)
        {
            SaveMember();
        }

        // We all know DataTemplates, don't we ?
        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 Segues 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 Intents 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

C#
namespace touchCPVanity
{
    public partial class CodeProjectMemberProfileViewController : UIViewController
    {

        public override void PrepareForSegue (UIStoryboardSegue segue, NSObject sender)
        {
            base.PrepareForSegue (segue, sender);

            // get the controller we're navigating to
            var memberArticlesController = segue.DestinationViewController as CodeProjectMemberArticlesViewController;

            if (memberArticlesController != null) {
                // and set it's properties
                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;
            // More code here
        }

        public override void ViewDidLoad ()
        {
            // When this code is called, our viewmodel's MemberId property is already set
            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.

C#
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);

            // More code here

            // Get the values from the Intent
            MemberId = Intent.Extras.GetInt (CodeProjectMemberProfileActivity.MemberIdKey);
            MemberReputationGraph = Intent.Extras.GetString (CodeProjectMemberProfileActivity.MemberReputationGraphKey);

            // And forward them to the viewmodel
            viewModel.MemberId = MemberId;

        }
    }
}

Windows Phone uses the concept of query strings borrowed from navigating the web in combination with events.

C#
namespace be.trojkasoftware.wpCPVanity
{
    public partial class CodeprojectMemberPage : PhoneApplicationPage
    {

        private void GotoPage(string page)
        {
            // On the page we want to navigate away from, we use the NavigationService
            //	Give it the URL of the page we want to go to
            //	In follwing example we get it from the CodeprojectMemberViewModel object
            //		public string TargetPage { get { return "/CodeprojectMemberProfilePage.xaml?id=" + member.Id; } }
            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)
        {
            // On the page navigated to, we parse off the parameters in the querystring
            base.OnNavigatedTo(e);
            String id = NavigationContext.QueryString["id"];
            viewModel.MemberId = int.Parse(id);
        }
    }
}

Conclusion

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:

Name Project size Code size Remark
Ripit 12.152 bytes    
monoCPVanity 22.204 bytes    
portableCPVanity 10.373 bytes    
droidCPVanity 107.506 bytes 27.240 bytes Exluded the Resources and Assets folders
touchCPVanity 150.752 bytes 34.959 bytes Excluded Resources folder and Storyboard file
wpCPVanity 177.825 bytes 81.195 bytes Exluded all XAML files and image files

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)