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

monoCPVanity:使用 Xamarin 的 CP Vanity

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (14投票s)

2015 年 1 月 6 日

CPOL

15分钟阅读

viewsIcon

37477

downloadIcon

292

从单个代码库(希望如此)实现 3 次 CP Vantiy。

Content

引言

CodeProject上已经有多篇文章实现了某种可视化会员分数以及最新文章和/或社区添加内容的应用程序,适用于各种平台,例如AndroidiOSWindows PhoneWindows,大部分都可以追溯到所有CP Vanity风格应用程序的母版,甚至具有不同的功能。那么,为什么还要再来一个呢?目前还没有使用C#作为iOS、Android和Windows Phone版本应用程序基础的,因此从或多或少(这是我想弄清楚的)单一代码库创建所有这三个应用程序。因此,本文并非关于如何在各种平台上进行操作,而是更多地关于如何在这些平台之间实现代码重用。

免责声明 1

出于实际原因,我选择了Windows Phone 7而不是8:我没有Windows 8机器。

免责声明 2

对于iOS和Android版本,您需要Xamarin。我希望源代码能够与Xamarin的免费版本一起编译,这也意味着接受该选择带来的限制。

  • 应用程序的最大尺寸
  • 不使用本地外部库
  • 不使用某些.NET功能,如WCF,System.Data.SqlClient

有关差异的完整列表,请访问Xamarin商店

上述限制导致我跳过了

  • 所有错误处理
  • 某些稳健实现所需的功能,例如取消异步任务的能力

结果是,这段代码实际上并不适合生产环境,更多地可以被视为概念验证。尽管如此,我认为它很好地展示了使用Xamarin框架进行移动应用程序开发的可能性。

我假设您对iOS、Android和Windows Phone 7平台有基本的了解。我不会讨论例如iOS平台上的Segue是什么,或者Android平台上的ActivityIntent是什么。

最终结果

在Youtube上

如果您迫不及待想看看结果如何,请观看这些Youtube视频。

通过截图

以下是Youtube视频的一些截图,三个平台上的相同屏幕并排显示。

会员列表

会员详情

会员文章

会员声望

切换到文章

文章

文章分类

社区

社区类别

业务代码

爬取网页

概念

目前CodeProject没有非常广泛的Web服务接口,所以要获取我们需要的数据,必须从网页上抓取。这里是代码共享的第一个机会。

  1. 抓取HTML页面的代码希望独立于平台。
  2. 项目类型也希望如此。

第一点是幸运地可行的:您可以在源代码的Ripit文件夹中找到抓取网页的所有代码。

第二点可以通过使用可移植类库类型项目在iOS和Android版本上实现。不幸的是,Windows Phone 7无法实现。但是,如果我选择了Windows Phone 8,我们也可以使用这种类型的项目。

代码

尽管这对于目前的讨论并不重要,但我仍然想稍微解释一下实现背后的想法。

在我实现CPVanity的原生iOS版本时,在抓取CodeProject网站的部分开发过程中,我注意到实现了大量的样板代码,并且还在重复自己。我想念C#的属性概念。因此,在实现C#版本时,我决定以正确的方式进行。Ripit库应运而生。

Ripit库允许您定义用于填充对象的数值。网页爬取基于正则表达式。为此,它提供了一组属性,允许您定义用于捕获属性值的正则表达式以及用作正则表达式源的网页URL。

这些属性是

  • HttpSourceAttribute:此属性允许定义一个URL以获取包含数据的网页。您可以在想要填充对象的类的多个实例上定义这些属性。它接受2个参数:一个用于标识属性实例的id,以及要使用的URL。
  • SourceRefAttribute:此属性应应用于类的属性,并定义类上的哪个HttpSourceAttribute应作为用于捕获该属性值的正则表达式的源。提供的id是所使用的HttpSourceAttribute的id。
  • PropertyCaptureAttribute:此属性也应应用于类的属性,并且是定义用于获取该属性值的正则表达式的实际属性。它有3个参数:正则表达式本身,捕获组的索引,最后是可选的。
  • CollectionCaptureAttribute:此属性必须应用于类,并且旨在用于填充集合。它只有一个参数:用于捕获用于填充对象值的文本片段的正则表达式。找到的每个片段都用于填充一个对象。因此,片段不是对象的值,而是包含必须进一步分析以填充对象的值的文本。
  • DefaultValueAttribute:如果PropertyCaptureAttribute是可选的且未找到值,则此属性定义要使用的值。

核心类是ObjectBuilder类。它提供了提供对象的方法,然后该对象将被填充数据。这些方法还有异步版本。

  • 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)

异步方法使用System.Threading.Task类,该类可在基于Xamarin的项目中使用。Windows Phone 7不支持Task类。幸运的是,有一个基于Mono实现的开源库,在此处可用,可在Windows Phone 7应用程序中使用。

ObjectBuilder类读取对象类型上定义的HttpSourceAttributes,下载页面并将它们存储在字典中,以Id作为键值。接下来,它迭代类的属性,获取SourceRefAttributePropertyCaptureAttribute,并使用它们来获取属性值。

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;
}

要填充集合,必须提供一个额外的参数,该参数提供用于创建用于填充集合的类的实例的工厂方法。

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;
}

用于分析RSSFeed的代码基于这个SO问题

CodeProject网站

概念

我们在所有三个平台上需要以下功能:

  1. 获取我们个人资料和我们感兴趣的人的数据。
  2. 维护一个我们感兴趣的人的列表。
  3. 获取最新发布文章的列表,最好可按类别选择。
  4. 获取最新讨论的列表,最好也可按类别选择。

同样,由于这里没有平台特定的内容,我们希望所有这些都放在一个公共库中。

不幸的是,这并不完全可能。

代表会员、他们的文章等的类是三个应用程序共有的。它们是简单的Plain-Old-C#-Object风格类,并放置在一个可共享到Xamarin项目中的可移植类库中。Windows Phone 7版本有自己的库,其中包含相同的文件,仅仅是因为它不支持可移植类库。然而,从概念上讲,这是可能的。但是有一个小小的注意事项:为了使其便携,我将会员的头像(实际上是一张图片)保存为包含原始图像数据的byte[]类型属性。只有当我们需要显示它时,我们才将其转换为特定于平台的表示。

要保存人员列表及其部分数据,我们需要某种存储。虽然代码本身可重用于iOS和Android平台,但底层实现不可重用。数据库访问通过Mono.Data.Sqlite完成,该库在可移植类库中不可用。Windows Phone 7平台甚至没有本地Sqlite支持,尽管有一个开源库可用。

获取会员的头像在大多数情况下是通用的,但将其转换为图像对象以在应用程序中显示是特定于平台的,正如在讨论表示会员的类时已经提到的那样。

在Xamarin上,存储头像到手机存储在Android和iOS上是通用的,但与使用隔离存储概念的Windows Phone完全不兼容。

我们通过使用以下功能来解决所有这些差异:

  1. 对于编译到特定平台代码中的通用代码,我们使用Xamarin和Visual Studio的文件引用功能。
  2. 对于最终分支到特定平台代码的通用代码,我们使用部分类。
  3. 对于完全不同的实现,我们定义一个公共接口,但有单独的实现。

选项1用于iOS和Android版本的数据库代码,以及将会员头像存储在文件系统中。选项3用于存储头像。我在此应用程序中未使用的一个选项是使用编译时常量和#define功能。

代码

如上所述,代表CodeProject网站的POCO类在Xamarin中共享在一个可移植类库中,在Windows Phone 7中共享在一个普通库中。

在文件系统上,我们有一个包含2个库项目的单个文件夹。

在Xamarin IDE中,有一个项目被两个特定平台项目引用。

在Visual Studio IDE中,有一个常规库项目。

作为该项目中类类型的示例,以下是CodeProjectMember类的代码。

namespace be.trojkasoftware.portableCPVanity
{
    [HttpSource(1, "https://codeproject.org.cn/script/Articles/MemberArticles.aspx?amid=Id")]
    [HttpSource(2, "https://codeproject.org.cn/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;
        }
    }
}

数据访问代码有一个用于Xamarin的单一代码库,但通过文件引用包含在iOS和Android项目中。Windows Phone 7项目有一个完全不同的实现。

在文件系统上,我们有一个没有项目文件但只有源文件的单个文件夹。在Xamarin IDE中,这些文件在各自的项目中都有引用。

实际实现位于monoCPVantity/Data/CodeProjectDatabase.cs文件中。

Windows Phone 7应用程序有自己的特定实现,导致文件系统上有单独的文件,并且这些文件包含在Visual Studio项目中。

我必须在此处补充一点:如果您查看Xamarin Tasky应用程序,您会注意到那里有数据库访问代码的共享。但是,为了实现这一点,他们不使用iOS和Android平台的本地SQLite支持,而是重新实现了Sqlite类,以.NET版本为基础。我选择不这样做,因为Xamarin Studio免费版本对应用程序的大小有限制。

同样,用于头像的文件存储代码在Xamarin平台上共享,但与Windows Phone 7的实现完全不同。

您可以在monoCPVantity/Util/FileStorageService.cs文件中找到Xamarin实现。

应用程序

显示值和执行命令

概念

iOS平台依赖于其内置的MVC模式。然而,大多数C#开发人员更熟悉MVVM模式,该模式严重依赖数据绑定,而iOS不支持这一点。Android有自己的叫做什么模式,也不支持数据绑定。

然而,让我们退一步。

当使用MVVM模式时,我们到底要做什么?其中一部分是关于显示值和将控件绑定到这些值。为此,我们创建一个ViewModel,它公开我们可以将View绑定到的属性。绑定部分是WPF的一个功能,但没有什么能阻止我们创建公开显示值属性的ViewModel,并自己编写“绑定”代码。

第二部分是关于在某些事件发生时执行操作。同样,在WPF应用程序中,通过绑定的魔力可以实现很多这一点,但没有什么能阻止我们以老式的方式通过事件处理执行操作。

这是本应用程序采取的方法:我创建了所有三个应用程序通过链接项目文件共享的ViewModel。对于iOS和Android版本中的属性,我们在特定平台代码中使用简单赋值。在Windows Phone版本中,我通过IPropertyChanged接口使用数据绑定。

这里采取的方法也是选择使应用程序在免费可用版本中可编译的直接结果。有可用的MVVM框架可以实现更多的代码共享,但那些会使iOS和Android应用程序的大小膨胀到Xamarin设定的限制之外。我知道,因为这发生在我身上。

代码

同样,与共享数据库和文件保存代码一样,在文件系统上,我们有一个没有项目文件但只有源文件的单个文件夹。在Xamarin IDE中,这些文件已在各自的项目中引用。这次,这些文件也被引用在Visual Studio项目中。

作为如何工作的示例,这里是显示CodeProject会员个人资料的ViewModel的一些代码。如您所见,会员数据的加载是通用的,一旦加载,我们就会调用一个委托,该委托将在UI中实现以更新UI。

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;
        }
    }
}

请注意我们如何将其实现为部分类。我们将在Windows Phone 7实现中稍后使用它。

在Android应用中使用此类如下所示。

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;
    }
}

在iOS应用中使用此类如下所示。

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;
    }
}

最后,通过部分类扩展来使用此类在Windows Phone 7应用中完成。

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;
        }

    }
}

导航应用

概念

所有三个平台的导航概念都非常不同。

iOS平台通过Segue的概念和SDK提供的方法来启动导航。在示例应用程序中,我们专门使用Segues。如果您想通过代码进行导航,请参阅这篇文章。在使用Segues时,您的viewcontroller中会调用一个方法PrepareForSegue,您可以在其中准备要导航到的viewcontroller,然后再显示它。参数传递是通过在目标控制器上定义属性并在上述方法中设置它们来完成的。返回值通过导航源实现的委托返回,并由导航目标调用。

Android平台使用Intent来建模导航。您基本上创建一个Intent,并在其中设置要导航到的Activity。参数传递是通过将它们打包到Intent中来完成的。此Intent也会被转发到目标Activity,然后您可以在其中解包参数。

最后,Windows Phone平台使用另一种方法:第一种可能性是使用查询字符串。第二种可能性是使用页面上的导航事件。这与iOS segue方法有些相似。这些可能性在此互联网上的文章“不同方式在Windows Phone 7页面之间传递值”中进行了描述。

从上述讨论中可以看出,从一个屏幕/页面到另一个屏幕/页面的导航并没有真正单一的方式。这就是为什么所有导航代码都实现在特定平台项目中的原因。然而,应该可以创建一个跨屏幕/页面传递参数的抽象,但由于Xamarin平台的免费限制,我没有花费时间在这上面。

代码

iOS使用Segue的概念。它们定义在Storyboard中,该Storyboard定义了应用程序的导航模型。通过在UIViewController中使用void PrepareForSegue (UIStoryboardSegue segue, NSObject sender)方法,您将在用户导航到另一个屏幕时收到通知,并有机会设置目标视图控制器上的属性。

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使用ActivityIntent的概念在它们之间导航。

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使用查询字符串的概念(借鉴了Web导航)结合事件。

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

结论

Xamarin平台对于代码重用看起来非常有前途,特别是对于包含大量业务代码的应用程序。当前版本甚至有一些用于共享更多代码的新功能。

不幸的是,最后一项功能在免费版本中不可用。

 

下表显示了此应用程序中各个项目的大小。

名称 项目大小 代码大小 备注
Ripit 12.152 字节    
monoCPVanity 22.204 字节    
portableCPVanity 10.373 字节    
droidCPVanity 107.506 字节 27.240 字节 不包括Resources和Assets文件夹
touchCPVanity 150.752 字节 34.959 字节 不包括Resources文件夹和Storyboard文件
wpCPVanity 177.825 字节 81.195 字节 不包括所有XAML文件和图像文件
© . All rights reserved.