Templated Controls in Xamarin.Forms – .NET Development Addict
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.
Contents
- The Control – creating the templated control
- The Template – creating a template for the control
- The Code Behind – working with the template views
- The Library – moving everything into a library / package
- The Resources – dealing with library resources
- The Registration – making sure the app can get those resources
- The Hack – a tiny little workaround for missing magic
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:
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.
- First, there is the
ControlTemplate
property. This is just where we place the new template that we want to use for the control. - 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.
- 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.
- 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:
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 theOnApplyTemplate
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.
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:
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:
- The new control template.
- The explicit style that applies that control template. This explicit style allows for extensions using the
BasedOn
property of styles. - The implicit style that will apply our explicit style to all of the instances of our control.
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.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK