7

Templated Controls in Xamarin.Forms – .NET Development Addict

 2 years ago
source link: https://dotnetdevaddict.co.za/2020/08/16/templated-controls-in-xamarin-forms/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Contents

Sometimes you want to create that awesome control, but also want the user to be able to totally customize it… What do you do? Create a templated control!

Xamarin.Forms has a pretty cool view that we can use: TemplatedView (API docs).

More information on how to use templated controls and page can be found in the docs: https://docs.microsoft.com/xamarin/xamarin-forms/app-fundamentals/templates/control-template

So, on this journey to create the best templated control, let us create a cool and exciting “confetti view” like this:

confetti.gif?w=203&h=300

In most cases we would just create a single view, no need to template anything. But, we would also like to try out templated views… and the user may want to place borders, backgrounds or some other fancy thing. If we can support this, then why not?

The Control

So, how does this work? Very simple! All you need is a new type that is going to be the control. Make sure it derives form TemplatedView:

public class SKConfettiView : TemplatedView
{
}

There we go! All done! Thanks for reading.

Just kidding, we need to show how to use it!

This is just like a normal control, so we can add it to our page as we would any other control:

<ContentPage ...
xmlns:controls="clr-namespace:SkiaSharp.Extended.Controls">
<controls:SKConfettiView />
</ContentPage>

When we run the app, we will have an invisible control in the app. We can’t see anything, but now we can get started on building up the control.

The Template

Now that we have the files we need, we can add the template. This is straight forward, and we just add a new <ControlTemplate> to the ControlTemplate property:

<ContentPage ...
xmlns:controls="clr-namespace:SkiaSharp.Extended.Controls"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms">
<controls:SKConfettiView>
<controls:SKConfettiView.ControlTemplate>
<ControlTemplate>
<skia:SKCanvasView x:Name="PART_DrawingSurface" />
</ControlTemplate>
</controls:SKConfettiView.ControlTemplate>
</controls:SKConfettiView>
</ContentPage>

So, let us have a look at all the moving parts in this XAML.

  1. First, there is the ControlTemplate property. This is just where we place the new template that we want to use for the control.
  2. Next, there is the <ControlTemplate> element. Just like with a data template, the actual views we want to add to the new control must go inside one of these.
  3. Now, there is <skia:SKCanvasView> element. This is just what we are going to use as our control. Not much now, but when a user overrides this template, they may want to wrap this is a frame to get a nice border.
  4. Finally, there is the x:Name="PART_DrawingSurface" attribute. This is what we will use to talk to the actual controls inside that we care about. In our control, we just care about the drawing surface, so we give that a name. If the user adds some cool frame, then we don’t need to worry about that.

As a demonstration of all this and a customization, I take this XAML (the red background is just so that we can see the surface):

<StackLayout>
<Label Text="Plain control" Margin="20" />
<controls:SKConfettiView Margin="20">
<controls:SKConfettiView.ControlTemplate>
<ControlTemplate>
<skia:SKCanvasView x:Name="PART_DrawingSurface"
BackgroundColor="Red" />
</ControlTemplate>
</controls:SKConfettiView.ControlTemplate>
</controls:SKConfettiView>
<Label Text="Custom control" Margin="20" />
<controls:SKConfettiView Margin="20">
<controls:SKConfettiView.ControlTemplate>
<ControlTemplate>
<Frame Padding="20">
<skia:SKCanvasView x:Name="PART_DrawingSurface"
BackgroundColor="Red" />
</Frame>
</ControlTemplate>
</controls:SKConfettiView.ControlTemplate>
</controls:SKConfettiView>
</StackLayout>

And it renders like this:

first-render.png?w=300&h=251

Even though we have two different controls with two different view structures, we still have the same essential control because both have the core element in the template:

<skia:SKCanvasView x:Name="PART_DrawingSurface" />

The Code Behind

Because we are making a control that we actually want to do something, we need some code. To do this, we hop on over to our C# code file.

The way templated controls work is very similar to other controls, but with one additional feature – they allow the actual control to use a template. But, this means that anyone can go and put anything in that template. So, how do we get access to those views in the template? By listening for when the template is applied.

When a template is applied to a control, there is a method (OnApplyTemplate) that we can override and start to access the things in the template by name. For example, in our control template, we have a SkiaSharp SKCanvasView that is named PART_DrawingSurface. Now, in our code file, we can use the GetTemplateChild to get that child view:

public class SKConfettiView : TemplatedView
{
protected override void OnApplyTemplate()
{
// get the child
var templateChild = GetTemplateChild("PART_DrawingSurface");
if (templateChild is SKCanvasView canvasView)
{
// subscribe to the paint even of the child so we can redraw
canvasView.PaintSurface += OnPaintSurface;
}
}
private void OnPaintSurface(object sender, SKPaintSurfaceEventArgs e)
{
e.Surface.Canvas.Clear(SKColors.Green);
// TODO: draw the real thing
}
}

When the framework determines it is time to build the actual control we see, it will invoke the OnApplyTemplate method, which we can then override to do things, such as subscribe to events or set properties. In our confetti control we are making, we just want to subscribe to the PaintSurface event and draw the confetti.

The GetTemplateChild method should only be called after the OnApplyTemplate method has been called.

If we run the code now, you can see that in both controls, the canvas has drawn a nice green background – even the one that has some weird frame around it! And this green is drawn from the event that we just subscribed to in the OnApplyTemplate method.

second-render.png?w=300&h=270

The Library

All done! Even though the confetti is not being drawn, our control is ready to package up. So, how do we do that? Again, very simple! We just create a new Xamarin.Forms class library and move the control there.

One this to remember is that when moving a control out of the main app project, we will have to update the XML namespaces and add the assembly= part:

<!-- from -->
xmlns:controls="clr-namespace:SkiaSharp.Extended.Controls"
<!-- to -->
xmlns:controls="clr-namespace:SkiaSharp.Extended.Controls;assembly=SkiaSharp.Extended.Controls"

Once the control is moved, we add a reference to this new library in our app, and make sure that it all works again.

However, if we package this library up and push to NuGet, every single developer out there will have to create their own control template. This is not how we want anyone to live! So, we ship our own template in the library!

The Resources

We could do a few things and place templates in a few places, but we want the control to use our default theme, but also allow for custom templates. So, in order to do this nicely, we can create a custom resource dictionary in our library. I decided to follow the pattern of UWP and WPF and place our “generic” control template in the “Themes\Generic.xaml” file.

Because there is no “resource dictionary” item template in Visual Studio, I just created a new XAML page and replaced the contents of both files.

In the XAML file, I specify the dictionary:

<ResourceDictionary xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:local="clr-namespace:SkiaSharp.Extended.Controls"
x:Class="SkiaSharp.Extended.Controls.Themes.Generic">
</ResourceDictionary>

In the C# code-behind, I change the base type:

public partial class Generic : ResourceDictionary
{
public Generic()
{
InitializeComponent();
}
}

Next, we need to move the template from the first control in our page into the resource dictionary XAML file. I went with three new entries:

  1. The new control template.
  2. The explicit style that applies that control template. This explicit style allows for extensions using the BasedOn property of styles.
  3. The implicit style that will apply our explicit style to all of the instances of our control.
<ResourceDictionary xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:local="clr-namespace:SkiaSharp.Extended.Controls"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
x:Class="SkiaSharp.Extended.Controls.Themes.Generic">
<!-- the control template for SKConfettiView -->
<ControlTemplate x:Key="SKConfettiViewControlTemplate">
<skia:SKCanvasView x:Name="PART_DrawingSurface" />
</ControlTemplate>
<!-- the explicit style that allows for extension -->
<Style x:Key="SKConfettiViewStyle" TargetType="local:SKConfettiView">
<Setter Property="ControlTemplate"
Value="{StaticResource SKConfettiViewControlTemplate}" />
</Style>
<!-- the implicit style that applies to all controls -->
<Style TargetType="local:SKConfettiView"
BasedOn="{StaticResource SKConfettiViewDefaultStyle}" />
</ResourceDictionary>

This is a bit more XAML as we could have just created a single implicit style that also has the control template, but the multiple parts allow for multiple extension points and for reuse of parts if need be.

In order to test all this fancy new XAML, we can update our page to only provide a custom template for the first control:

<StackLayout>
<Label Text="Plain control" Margin="20" />
<controls:SKConfettiView Margin="20" />
<Label Text="Custom control" Margin="20" />
<controls:SKConfettiView Margin="20">
<controls:SKConfettiView.ControlTemplate>
<ControlTemplate>
<Frame Padding="20">
<skia:SKCanvasView x:Name="PART_DrawingSurface" BackgroundColor="Red" />
</Frame>
</ControlTemplate>
</controls:SKConfettiView.ControlTemplate>
</controls:SKConfettiView>
</StackLayout>

When we run the app now, the first control is missing everything! What has happened? Is all lost? No! This is because the app does not know about this fancy new style we just created.

The Registration

There are a couple of ways to solve this. First we could just import the resource dictionary into the page or into the app. Because I want this style to apply to all parts of my app, I went with the app resources:

xmlns:themes="clr-namespace:SkiaSharp.Extended.Controls.Themes;assembly=SkiaSharp.Extended.Controls"
x:Class="SkiaSharpDemo.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<themes:Generic />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

We are basically done now. No matter what we do, we are importing our styles and templates, and we also allow for total customization.

We had to do work to get the styles to apply! We never want to do work. If we ship this out to NuGet, every single developer will forget to add the styles, we will get a new issue on our repository, we will be sad! No! We want magic!

In UWP and WPF, the path that we used for our XAML resources (Themes\Generic.xaml) is actually pretty special. So special in fact they have a docs page on this:

Theme-level dictionaries are stored in a subfolder named Themes. The files in the Themes folder correspond to themes. For example, you might have Aero.NormalColor.xaml, Luna.NormalColor.xaml, Royale.NormalColor.xaml, and so on. You can also have a file named Generic.xaml. When the system looks for a resource at the themes level, it first looks for it in the theme-specific file and then looks for it in Generic.xaml.

Now, it would be wonderful if Xamarin.Forms also was able to do something like this, wouldn’t it? Well, I agree! So I just opened an issue for this exact reason. I will try get it implemented.

There is always a but! What about today? We can’t wait for some feature to ship! We need that confetti, now! We could throw up our hands and say “oh, well, ’tis what it is”. But we won’t! We come up with solutions!

The Hack

One way that seems to work just fine is to register the theme inside the control. And, we can do this by inserting our theme in the very same MergedDictionaries property that we just used.

Before we start hacking, we can remove that pesky code we just added to the App.xml and we will see our control disappear again. But not to worry, we will use magic to fix it.

This magic is pretty straight forward, in the Generic resource dictionary with out styles, we add a nice method to the code-behind:

public partial class Generic : ResourceDictionary
{
private static bool registered;
public Generic()
{
InitializeComponent();
}
internal static void EnsureRegistered()
{
// don't do extra work
if (registered)
return;
// get the dictionary if we can
var merged = Application.Current?.Resources?.MergedDictionaries;
if (merged != null)
{
// check to see if we are added already
foreach (var dic in merged)
{
if (dic.GetType() == typeof(Generic))
{
registered = true;
break;
}
}
// if we are not added, add ourselves
if (!registered)
{
merged.Add(new Generic());
registered = true;
}
}
}
}

Finally, we can call this method in our control’s constructor:

public SKConfettiView()
{
Generic.EnsureRegistered();
}

Only now we are done! We now have a very nice library that works automatically and is awesome for customization.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK