zoukankan      html  css  js  c++  java
  • Binding a Xamarin.Forms WebView to ReactiveUI View Model using Custom Type Converters

    引用:https://jamilgeor.com/binding-a-xamarin-forms-webview-to-reactiveui-view-model-using-custom-type-converters/

    Introduction

    In this article, we will expand on our demo app, by displaying article content inside of a WebView after the user is navigated to the ArticleView.

    To make things more interesting, because we are pulling in content from this blog, we will be using HTMLAgilityPack to strip out any unnecessary HTML that we don't want to be displayed inside of our app.

    We'll also be utilising a ReactiveUI custom type converter to bind our WebView to raw HTML content.

    At a high level our ArticleViewModel is going to do the following:

    1. Download HTML from URL specified in the Article object that we are storing in our ViewModel
    2. Cache the HTML content
    3. Parse HTML to strip out unwanted tags, and fix other related issues
    4. Map content to WebView

    Setup

    Install the HTMLAgilityPack NuGet package into all three projects i.e. Droid, iOS, and PCL projects. We'll be using this to clean up the HTML that we download from this blog.
    https://www.nuget.org/packages/HtmlAgilityPack

    Parsing HTML

    A WebView can take two different data sources, the first being a website URL, and the second being a raw HTML string. In our case we want to use the second method, this is because we'd like to modify the HTML from the articles before we display the article in our View.

    I won't go into the parsing code detail, but at a high level, you can see what is happening in the code snippet below.

    public interface IHtmlParserService
    {
        string Parse(string content, string baseUrl);
    }
    
    public class HtmlParserService : IHtmlParserService
    {
        public string Parse(string content, string baseUrl)
        {
            var document = new HtmlDocument();
    
            document.LoadHtml(content);
    
            ReplaceRelativeUrls(document, baseUrl);
    
            RemoveRedundantElements(document);
    
            return document.DocumentNode.OuterHtml;
        }
        ...
    }
    

    The full source code for this class can be found here.

    ViewModel

    We'll now setup our ViewModel. The first thing we need to do is create a new property that the View will bind to called Content. This property will simply store the parsed HTML content that we want to display in our View.

    string _content;
    public string Content {
        get => _content;
        set => this.RaiseAndSetIfChanged(ref _content, value);
    }
    

    We now need to implement our methods for fetching our content from the blog, and also the method that we'll use to map the parsed HTML content to our Content property.

    IObservable<string> LoadArticleContent()
    {
        return BlobCache
            .LocalMachine
            .GetOrFetchObject<string>
            (CacheKey,
             async () =>
             await _articleService.Get(Article.Url), CacheExpiry);
    }
    
    void MapContentImpl(string content)
    {
        Content = _htmlParserService.Parse(content, Configuration.BlogBaseUrl);
    }
    

    We now want to call our new LoadArticleContent method from inside of our constructor to initialize the Content property that the View will be bound to.

    public ArticleViewModel(IScreen hostScreen, Article article)
    {
        ...
    
        LoadArticleContent()
            .ObserveOn(RxApp.MainThreadScheduler)
            .Catch<string, Exception>((error) => {
                this.Log().ErrorException($"Error loading article {Article.Id}", error);
                return Observable.Return("<html><body>Error loading article.</body></html>");
            })
            .Subscribe(MapContentImpl);
    }
    

    The above code simply does the following:

    1. Call LoadArticleContent method
    2. Ensure that the following code runs on the main UI thread
    3. Catch any exceptions, and return an error message
    4. Call the MapContentImpl method and pass the result of LoadArticleContent or the Error message

    Custom Type Converters

    Now that our ViewModel is all setup, we just need to bind the Source property of our WebView to our Content property on our ViewModel.

    Because the Source property of a WebView uses a type that doesn't have a default binding converter provided by ReactiveUI, we'll need to create our own.

    To do this we just need to create a class that implements the IBindingTypeConverter interface.

    public class HtmlBindingTypeConverter : IBindingTypeConverter, IEnableLogger
    {
        public int GetAffinityForObjects(Type fromType, Type toType)
        {
            return fromType == typeof(string) && toType == typeof(WebViewSource) ? 100 : 0;
        }
    
        public bool TryConvert(object from, Type toType, object conversionHint, out object result)
        {
            try
            {
                result = new HtmlWebViewSource { Html = (string)from };
                return true;
            }
            catch (Exception error)
            {
                this.Log().ErrorException("Problem converting to HtmlWebViewSource", error);
                result = null;
                return false;
            }
        }
    }
    

    The GetAffinityForObjects method simply validates that a the from and to properties can be converted by our type converter. If it can't then we should return 0 otherwise, we should return any number greater than 0. If there are two converters that can handle the types that are passed to it, then the number returned by this method will determine which one is selected.

    The TryConvert method does the actual conversion from one type to another.

    You can read more about type converters here:
    https://reactiveui.net/docs/handbook/data-binding/type-converters

    Now that we have created our type converter, we need to register it with the Splat dependency resolver. We do this in our AppBootstrapper class.

    public class AppBootstrapper : ReactiveObject, IScreen
    {
    ...
        private void RegisterParts(IMutableDependencyResolver dependencyResolver)
        {
            ...
            dependencyResolver.RegisterConstant(new HtmlBindingTypeConverter(), typeof(IBindingTypeConverter));
        }
    ...
    }
    

    Binding the View

    All we need to do now is setup the binding in our view.

    public partial class ArticlePage : ContentPage, IViewFor<ArticleViewModel>
    {
        readonly CompositeDisposable _bindingsDisposable = new CompositeDisposable();
        
        ...
        protected override void OnAppearing()
        {
            base.OnAppearing();
    
            this.OneWayBind(ViewModel, vm => vm.Content, v => v.ArticleContent.Source).DisposeWith(_bindingsDisposable);
        }
        ...
    }
    

    Summary

    In this article, we've created a custom type converter for binding raw HTML content to a Xamarin.Forms WebView.

    ReactiveUI Custom Type Converter

    Full source code for this article can be found here:
    https://github.com/jamilgeor/FormsTutor/tree/master/Lesson09

    References

    https://www.nuget.org/packages/HtmlAgilityPack
    https://reactiveui.net/docs/handbook/data-binding/type-converters

  • 相关阅读:
    两周深度学习计划(二)
    两周深度学习计划(一)
    [Spark性能调优] 第二章:彻底解密Spark的HashShuffle
    [Spark性能调优] 第一章:性能调优的本质、Spark资源使用原理和调优要点分析
    [Spark内核] 第33课:Spark Executor内幕彻底解密:Executor工作原理图、ExecutorBackend注册源码解密、Executor实例化内幕、Executor具体工作内幕
    [Spark内核] 第32课:Spark Worker原理和源码剖析解密:Worker工作流程图、Worker启动Driver源码解密、Worker启动Executor源码解密等
    [Spark内核] 第31课:Spark资源调度分配内幕天机彻底解密:Driver在Cluster模式下的启动、两种不同的资源调度方式源码彻底解析、资源调度内幕总结
    [Spark内核] 第30课:Master的注册机制和状态管理解密
    [Spark内核] 第29课:Master HA彻底解密
    [Spark内核] 第28课:Spark天堂之门解密
  • 原文地址:https://www.cnblogs.com/mschen/p/11289903.html
Copyright © 2011-2022 走看看