June 12, 2019

Nugeting a Custom Visual In Xamarin Forms

With a custom Visual in a package it's easy to carry a consistent set of renderers between apps and platforms for consistent branding.

In my previous post I covered how to Extend Visual Material in Xamarin.Forms. The post covered the benefits of Xamarin.Forms Visual with the Material Renderers and how to create a Custom Visual. This works great but all the code was tightly couple to that one app. If you're a Xamarin developer there's a solid chance you like reusing code and you feel guilty whenever you copy and paste it around.

With a custom Visual in a package it's easy to carry a consistent set of renderers between apps and platforms for consistent branding.

In this post I'm taking all the goodness from my previous App, distilling it down to the import stuff and bottling it up in a Nuget that I can add to all my apps with only a few clicks. The final package is publicly available on Nuget for all at https://www.nuget.org/packages/Lachlan.Visual/. If you want your package to be private for use only within your team you could also host it on a private Nuget feed like MyGet, GitHub Package Registry(they should have called it NugitHub or GetHub). A local or network drive is a super easy solution for small teams in a secure network.

Source code for this nuget is at https://github.com/lachlanwgordon/Lachlan.Visual.

In my previous post the Material Renderers were exported in a less than ideal way which has a long list of exports that could lead to your app missing out on new Material Renderers. I've since found a cleaner way to automatically export the renderers so you won't get left behind.

There are lots of different ways to configure a package. I'm using Visual Studio Mac 2019 with a project structure that I've found to work well. If you're using Visual Studio Windows it should be a similar process.

Step 1. File > New > Visual

Sorry, not quite as easy as File > New > Visual but not far off.

In the New Project Dialog select Multiplatform Library from the Multiplatform Library section and click Next.

A little tortological but still logical.

Name the Project, mine's called Lachlan.Visual. Select Platform specific. Click next, then Create.

I originally called it Lachterial as a portmanteau of Lachlan and Material but it made me feel ill so I had to start again and redo all these screenshots.

Step 2. Solution Structure

The template starts out with four projects.

  • .Android - This is for Android specific code such as renderers
  • .iOS - iOS specific codes such as renderers
  • .Nuget - This lil project has no code but does all our packaging magic
  • .Shared - Code that is shared across platforms. Useful in many libraries/apps but not what I want to use for this.

Delete the Shared Library. There'll be a few warnings, just keep clicking Delete.

Right click on the solution > Add > Add new project.

Choose .NET Standard from Multiplatform Library section and click Next

Choose a .Net Standard version and click next.  I'm selecting .NET Standard 2.0 because it's the default and 2.0 must be better than 1.6 or they wouldn't have made it.

Name your project anything you like, I'm calling mine Lachlan.Visual.Standard and then click Create.

In your .Nuget project, right click on Edit References. Change to the Projects tab and a tick next to your .Net Standard project and click OK. Without this step your Nuget could only be installed in iOS and Android projects but not in the .Net Standard Package where you write your UICode.  

Now I've got a project that looks like...

this

Step 3. Just add Nugets.

I heard you like Nugets so let's put some Nugets in your Nuget.

In your Android project right click on Packages, click Add Nuget Packages.

Install Xamarin.Forms and Xamarin.Forms.Visual.Material and choose the lowest version you want to support. I recommend 4.0.0.425677 because it's the first stable release of 4. If you have an app that runs Xamarin.Froms 3.6 you could target that instead but you'll probably be happier if you just upgrade to 4.

There were breaking changes in the Android Material renderer constructors between 3.6 and 4.0 so supporting both with one nuget version is difficult.

That just installed a whole lot of packages. Don't worry, they were in your app anyway because Forms depends on them. As long as the apps that add this package have the matching Xamarin.Forms you'll have no problems*.

Repeat for your iOS project and you .Net Standard project.

Step 4. Add your Visuals

In your .Net Standard project add a file that ends with the name Visual.cs, I'm calling mine LachlanVisual.cs.

Make the class implement IVisual and add a using statement for Xamarin.Forms.

using Xamarin.Forms;

namespace Lachlan.Visual
{
    public class LachlanVisual : IVisual
    {
        public LachlanVisual()
        {
        }
    }
}

Repeat this step in your Android and iOS projects. I'm sorry, I know this feels a lot like we're not sharing much code but it's worth it for how much you can save in your apps.

Step 5. Add some renderers

LachlanVisual is going to be made up almost exclusively of Material renderers with just a couple of my own which subclass the material renderers. For now I'll just add an AnyCaseButtonRenderer in my iOS project and one in my Android project with very similar implementations.

iOS

public class AnyCaseButtonRenderer : MaterialButtonRenderer
{
    protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
    {
        base.OnElementChanged(e);
        if(Control != null)
        {
            Control.UppercaseTitle = false;
        }
    }
}

Android

public class AnyCaseButtonRenderer : MaterialButtonRenderer
{
    public AnyCaseButtonRenderer(Context context) : base(context)
    {
    }

    public AnyCaseButtonRenderer(Context context, BindableObject element) : base(context, element)
    {
    }

    protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
    {
        base.OnElementChanged(e);
        if(Control != null)
        {
            Control.SetAllCaps(false);
        }
    }
}

If you have any other renderers you want, go ahead and add them now or come back later.

Step 6. Referencing Material Renderers

If we nugeted it all up right now, we would have a Visual with all our custom renderers which might be all you're after.  For the most part I like the look of the Material renderers so I'm going to include the rest of them in stock form.

We have three (or maybe many more) options for including the Material renderers. The more time I spend messing around with visual and the deeper I go into the Xamarin Forms source code, the more options I come up with. These are:

  1. Subclass every material renderer, even if you don't add any functionality. This was my first succesful implementation of an extended VisualMaterial. I haven't blogged on this way because it is no longer my preferred option. It has merit in that it is a similar process to how we always used to work with renderers. When you open up a platform project you see a folder full of renderers that tell you where they come from. The downside is that it's a whole lot of boiler plate and if a new renderer comes along you are likely to miss out. You can see an example of this in an older version of my orginal sample at https://github.com/lachlanwgordon/ExtendedVisualSample/commit/0a1d61aa99fb133e7ac1f5373430c2aa1ef76542
  2. A big old pile of export statements. Put all of your export statements in one file per platform. This is how I implemented it for my previous blog post. It works great and doesn't come with too much boiler plate. It carries the same risk as option 1 but is easier to maintain so you're more likely to update it. This option is also risk free for the linker but not self updating.
  3. This method uses reflection to export all Material Renderers to your visual. When a new Material Renderer comes along, your custom Visual will automatically include it as soon as the app gets its Visual.Components updated. No need to update your CustomVisual Nuget. The downside is that Apps that consume this libray have to add a --linkskip mtouch argument if they want to use LinkAll. --linkskip isn't big deal but it's easily foregetable, I wish I didn't have to ask users to do this. If you have any experience with these situations please contact me.

I'm going for option 3 here because it's future proof and the difference between the code being linked away and included are negligible. I've experimented with --linkskip and if I link away the entire assembly – including the important stuff – it only saves 1022 bytes(I could have said "less than 1 kilobyte" but it's only 2 bytes shy so that would feel a little dirty).

Inside LachlanVisual add a static void Init() method that looks like this:

public static void Init()
{
    var materialAssembly = Assembly.GetAssembly(typeof(Xamarin.Forms.Material.iOS.MaterialButtonRenderer));//Get the assembly where the MaterialRenderers live

    var name = "Xamarin.Forms.Material.iOS";//investigate a type safe way
    var baseRendererTypes = materialAssembly.ExportedTypes.Where(x => x.IsClass && x.Namespace == name && x.Name.Contains("Renderer"));//Get all the Material Renderers

    foreach (var baseRendererType in baseRendererTypes)//Iterate over every material renderer
    {
        var baseRendererElementProperty = baseRendererType.GetRuntimeProperties().FirstOrDefault(x => x.Name == "Element");//Find the type of the XamarinForms View that the render looks after
        Xamarin.Forms.Internals.Registrar.Registered.Register(baseRendererElementProperty.PropertyType, baseRendererType, new[] { typeof(LachlanVisual) });//Register the renderer. This call is equivalent to the Export statements we usually put in our renderers.
    }

}

This code is so heavilly comented because it's quite different to any code I've written before and is delving into the undocumented parts of Xamarin.Forms. The jist of it is to get all the material renderers and add them to your visual automatically.

The Android code is almost identical. The different namespace and assembly names are to be expected but the reflection code is a little weird. Interestingly the Element property is protected in Android renderers but public in iOS. I'm not sure if there is a deliberate reason behind this or just a symptom of different maintainers. The Element property is also often inherited so the FlatenHierarchy flag is required.

public static void Init()
{
    var materialAssembly = Assembly.GetAssembly(typeof(Xamarin.Forms.Material.Android.MaterialButtonRenderer));//Get the assembly where the MaterialRenderers live

    var name = "Xamarin.Forms.Material.Android";//investigate a type safe way
    var baseRendererTypes = materialAssembly.ExportedTypes.Where(x => x.IsClass && x.Namespace == name && x.Name.Contains("Renderer"));//Get all the Material Renderers

    foreach (var baseRendererType in baseRendererTypes)//Iterate over every material renderer
    {
        var baseRendererElementProperty = baseRendererType.GetProperty("Element", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy);   //Find the type of the XamarinForms View that the render looks after. In Android this is often protected and/or inherited
        if (baseRendererElementProperty != null)
        {
            Xamarin.Forms.Internals.Registrar.Registered.Register(baseRendererElementProperty.PropertyType, baseRendererType, new[] { typeof(LachlanVisual) });//Register the renderer. This call is equivalent to the Export statements we usually put in our renderers.
        }
    }
}

Step 5. Build it and Ship it

That's all the code for your package. Now we just need to build it, post it to a nuget feed and add it to all our apps.

Build the package by clicking the little hammer at the top left, it's where the play button usually sits.

For testing's sake now I'll push my package to a private feed on my local disk. This is surprisingly easy and useful. I was expecting hosting a nuget feed to include include installing a server, editing .conf files, fighting with port conflicts... You do have to use the terminal but it's fine.

Open terminal

Navigate to the bin folder of your .nuget package. The command will be something like cd /users/lachlangordon/Projects/Lachlan.Visual/Source/Lachlan.Visual.NuGet/bin/Debug Depending on where you keep your files and what your project is called. If you built in Release mode, navigate to the release build instead.

Add it to your local nuget folder with a command along the lines of
nuget add Lachlan.Visual.1.0.0.nupkg -source /users/lachlangordon/Projects/LachlaNugets.
Nuget add will create the folder if necessary, initialise it as a nuget feed and install your package.

Step 6. Consumption

Now open up an app that uses Xamarin.Forms 4 or later(or 3.6 if you chose 3.6 when installing XF in your package). If you don't have an app ready to go make a new one – the shell template is great. I'll be reusing the app that I used in my previous post which has the same page repeated with different Visuals. For this I'll add a 4th tab for LachlanVisual.

In your app, right click on you .Net Standard project and click Add > Add Nuget Package.

At the top left, there's a little drop down box that says nuget.org (unless you're already using a custom feed). Select the last option in the list "Configure Sources..."

At the bottom right, click "Add"

Click "Browse..." and select the folder you used in when adding the nuget in the terminal. For me that was /Users/lachlangordon/Projects/LachlaNugets.

Click "Add Source".

Click "OK"

Your local nuget source will now be available anytime you're adding nugets to any projects. Be aware that if you use CI for your builds or if anyone else uses your code they won't have access to your local repo (sorry if that sounds like stating the bleeding obvious but it's broken my CI more than once(Please AppCenter, I just want to ship an App to the AppStore using the XamarinForms nightly build, I promise, it'll be fine)).

From the source drop down select your new source.

You should now see your nuget as the only item in list. Click "Add Package".

Add this package to your iOS and Android projects as well.

Now let's drop in on our AppDelegate to call Init() on the new visual.

My FinishedLaunching ends up as:

public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
    Xamarin.Calabash.Start();
    Forms.SetFlags("Shell_Experimental", "CollectionView_Experimental");
    Lachlan.Visual.LachlanVisual.Init();
    global::Xamarin.Forms.Forms.Init();
    FormsMaterial.Init();
    LoadApplication(new App());

    return base.FinishedLaunching(app, options);
}

And my main activity's OnCreate is:

protected override void OnCreate(Bundle savedInstanceState)
{
    TabLayoutResource = Resource.Layout.Tabbar;
    ToolbarResource = Resource.Layout.Toolbar;

    base.OnCreate(savedInstanceState);

    Xamarin.Essentials.Platform.Init(this, savedInstanceState);
    Forms.SetFlags("Shell_Experimental");
    Lachlan.Visual.LachlanVisual.Init();
    global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
    FormsMaterial.Init(this, savedInstanceState);
    LoadApplication(new App());
}

Side note: typing Lachlan.Visual.LachlanVisual felt silly but I enjoyed it.

Now we need to tell our UI to use this new visual that we've lovingly crafted. All you have to do is add Visual="Lachlan" to any control you want to use the new renderer, or on a ContentPage, or on a Shell.

My new Shell with four pages with different visuals is now:

<?xml version="1.0" encoding="UTF-8"?>
<Shell
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    Visual="Custom"
    FlyoutBehavior="Disabled"
    x:Class="VisualExtension.AppShell"
    xmlns:pages="clr-namespace:VisualExtension.Pages">
    <ShellItem>
        <pages:FormPage Title="Default" Visual="Default"/>
        <pages:FormPage Title="Material" Visual="Material"/>
        <pages:FormPage Title="Custom" Visual="Custom"/>
        <pages:FormPage Title="Lachlan" Visual="Lachlan"/>
    </ShellItem>
</Shell>

I hit play and end up with:

Exactly what I wanted.

Step 6a. Check your Linker

The Linker is your friend, it strips away all the code you don't use when you do a release build. In my experience this usually knocks about 30mb off your final package size. If you've got your linker set to "Don't Link" or "Link SDK Assemblies Only", you don't need to worry because the Linker won't touch your nuget code. This next little bit of the post is only important if you use "Link All".

The problem we run into is with the reflection in the Init() method which uses every material renderer but doesn't mention any of them by name so the linker doesn't think they've been used at all. I could explicitly name all of the material renderers but this would get out of date quickly. For example, this library is built on Xamarin.Forms 4.0 but when I use it in an app with 4.1 I get to use the material renderer for the brand new CheckBox.

Double click on your app's iOS project.

Open the iOS Build page.

Change the configuration to Release and the Platform to iPhone.

If the Linker Behaviour is "Link All" add --linkskip=Lachlan.Visual to the Additional mtouch arguments field. With your nuget's assembly name in place of Lachlan.Visual

Change the Platform to iPhoneSimulator.

Once again, if the Linker Behaviour is Link All, add the same argument.

Click OK.

The process is very similar for Android. In the Android Build settings, in Release configuration, if you use "Link all assemblies" add Lachlan.Visual to the ignored assemblies.

Step 7. Fix Bugs

Chance are you made a mistake. I sure did when figuring this out, several times. So it's worth mentioning a few pain points.

  • When you go back to your nuget project to edit or add renderers, you'll need to bump the version number before adding it to nuget again. This is done in the option for the .NuGet project.
  • If you get an error about apisomething.exe failed with exit code -1, it's todo with multiple VS instances being open at once. Quit all open Visual Studios, delete bin and obj and try again.

Step 8. Lets Go Public

Assuming everything went perfectly, let ship it straight to Nuget.

We'll need to update the metadata on our project first to stop the nuget validator getting grumpy with us and to help people find and use the nuget.

Open the project options for your .NuGet package and fill out the details, mine ended up as:

My license url is a link to the MIT licence file that's in this project's public git repository.

Head over to nuget.org, create an account or use your Microsoft account.

At the top right of the screen where you see your name, expand the drop down and click Upload Package.

Upload the .nupkg found in ..../projecturl/bin/release.

For now, ignore the licence warning. We're currently in a weird limbo – Nuget is moving away from license urls but VS Mac doesn't support the new system yet.  For this blog I'll glaze over the differences and call them "out of scope" and stick to the old version. The short story is: licenses used to be easy, soon they will be easier and more flexible, right now they're fiddly.

Scroll down and click submit and you're done. Nuget will take about an hour to index and then you'll be able to add your package to your apps.

*I said no problems, no one can promise that. Dependancies trees are tricky.