Introduction
The Windows Phone application bar is a quintessential part of the Windows Phone user experience. I couldn’t contemplate building a Windows Phone app without leveraging the built-in ApplicationBar.
In this article you look at defining a custom AppBar in XAML; binding it to collections of command objects and at directly binding AppBar items to viewmodel properties. You see how to employ platform specific rendering to display an AppBar on iOS, Android, and Windows Phone. You learn how the Calcium AppBar is multi-page aware and can be placed within multiple pages hosted in a CarouselPage
or a TabbedPage
. You see how to display a custom menu in iOS and Android. Finally, you look at how to register a custom platform-specific view renderer.
Calcium’s AppBar for the Windows Phone platform leverages the Windows Phone SDK’s built-in ApplicationBar
to provide binding support and to allow multiple application bars within Pivot
and Panorama
controls defined using XAML. In addition, Calcium’s AppBar has support for toggle buttons and toggle menu items, navigation buttons and so forth. This goes beyond what you can expect to achieve when creating a native renderer for the Windows Phone SDK’s ApplicationBar
control.
NOTE. The AppBar
control for Xamarin Forms presented in this article is a work in progress. As far a being production ready, it's probably not quite there yet.
Before you begin, if you haven’t already, I recommend reading the first article in the series before tackling this one.
Articles in this Series
Source Code for Calcium and Example Apps
The source code for Calcium for Xamarin.Forms and the examples is located at https://calcium.codeplex.com/SourceControl/latest
There are various solutions throughout the repository. The one you are interested in is located in the \Trunk\Source\Calcium\Xamarin\Installation directory, and is named CalciumTemplates.Xamarin.sln.
The structure of the CalciumTemplates.Xamarin solution is shown in Figure 1. You can see the example 'template' projects have been highlighted. These are the projects that contain much of the example source code that is presented within this article series.
Figure 1. Structure of CalciumTemplates.Xamarin Solution
Using the AppBar
The AppBar
contains a collection of buttons and menu items, and is rendered differently depending on the platform which it is running on. If your app is running on Windows Phone, the AppBar
is rendered using the built-in ApplicationBar
control. When running on iOS and Android, the AppBar
is rendered using the Xamarin Forms toolbar (for the buttons) and dialogs for the menu items.
The AppBar
can be bound to lists of button and menu item command or defined purely in XAML, as demonstrated in Listing 1.
Listing 1. Defining an AppBar in XAML
<calcium:AppBar>
<calcium:AppBar.Buttons>
<calcium:AppBarItem Text="Foo" Tap="FooButtonHandleTap"
IconUri="/Views/MainView/Images/AppBar/Check.png" />
<calcium:AppBarItem Text="Bah" />
</calcium:AppBar.Buttons>
<calcium:AppBar.MenuItems>
<calcium:AppBarItem Text="MenuItem1" />
</calcium:AppBar.MenuItems>
</calcium:AppBar>
When I began putting together the code for this article, I decided to go beyond what I had already created in Calcium for Windows Phone. I've constructed the AppBar
in a manner that allows developers to better separate the UI from its viewmodel. The AppBar
can be bound to a collection of button commands and a collection of menu item commands. I chose to allow binding to a collection of commands, rather than a bespoke model class, because the commanding API is well understood and Calcium's UICommand
objects have all that is needed to represent a button or menu item.
In WPF, RoutedUICommands
can be used to encapsulate the textual display property of the command. The advantage of this is that the text of a button, for example, can be updated from within the command logic. When Silverlight arrived, the notion of placing UI specific properties within an ICommand
implementation was deemed unfavorable. Indeed, RoutedUICommand
was not included in the FCL. It was left solely to the UI to display text according to its own state or of its viewmodel. This, I believe, has led to a tendency for business logic to leak into the view layer. The idea of a command may be orthogonal to notions of text and icons, but a command backing a button control, with text or an icon, is so common place that you can’t deny it is a good candidate to be combined into a single type. This, I suppose, is where perceived best practice and pragmatism collide.
When evaluating these kinds of design decisisions, in my view, if the cost is negligible, yet a payoff exists, then you should err on the side of pragmatism, and make API decisions that favour ease of development rather than perceived value that can result in internal complexity.
Let's move on to an overview of the binding capabilities of the AppBar
. The MainViewModel
in the downloadable sample code contains two ObservableCollection
s containing objects of type IUICommand
. IUICommands
extend the familiar ICommand
interface to include a property for text, for visibility, and an icon URL. The properties of an object implementing IUICommand
are able to be mapped to a buttom or a menu item in the AppBar
. In the following excerpt, an IUICommand
allows the user to tap to navigate to the HubView
page:
navigateToHubCommand = new UICommand(NavigateToHub)
{
Text = AppResources.MainView_AppBar_Buttons_Hub
};
appBarButtonCommands.Add(navigateToHubCommand);
The NavigateToHub
method resolves the HubView
page, which is built-up by the IoC container, and the INavigation
instance navigates to the HubView
, as shown:
async void NavigateToHub(object arg)
{
var hubView = Dependency.Resolve<HubView>();
Navigation.PushAsync(hubView);
}
A menu item is placed in the AppBar
by creating another UICommand
object. This time, Calcium’s Dialog Service presents the user with a message prompt. The DialogService
is responsible for presenting dialogs on Windows Phone, iOS, and Android.
var menuCommand = new UICommand(_ => DialogService.ShowMessageAsync("Menu Item 1 tapped."))
{
Text = "Menu Item 1"
};
appBarMenuCommands.Add(menuCommand);
An XML namespace declaration is included on the MainView page, like so:
xmlns:calcium="clr-namespace:Outcoder.UI.Xaml;assembly=Outcoder.Calcium"
And finally, an AppBar
is defined on the MainView page, which is data-bound to the two ObservableCollection
s, as shown:
<calcium:AppBar
ButtonCommands="{Binding AppBarButtonCommands}"
MenuCommands="{Binding AppBarMenuCommands}" />
This is then materialized on all three views as shown in Figure 2. Notice the location of the rendered AppBar
buttons across the three platforms.
Figure 2. An AppBar
displayed on Windows Phone, iOS, and Android.
Building a Xamarin Forms Custom AppBar
You can create a custom Xamarin Forms control or view, as they are known, by subclassing the Xamarin.Forms.View
class.
The Calcium AppBar
for Xamarin Forms is composed of an ObservableCollection
of buttons and an ObservableCollection
of menu items. Buttons and menu items are both represented by an AppBarItem
class that implements the IAppBarItem
interface. IAppBarItem
has the following members:
- string Text
- Uri IconUri
- bool IsEnabled
- void PerformTap()
The base implementation of the IAppBarItem
interface is the AppBarItemBase
abstract class. See Figure 3. The AppBarItem
class adds commanding support.
Figure 3. The AppBar contains collections of IAppBarItem
objects.
The AppBarItemBase
class contains two bindable properties that are initialized in the class’s static constructor, like so:
static AppBarItemBase()
{
TextProperty = BindableProperty.Create(
"Text", typeof(string), typeof(AppBarItemBase), string.Empty, BindingMode.TwoWay);
IconUriProperty = BindableProperty.Create(
"IconUri", typeof(Uri), typeof(AppBarItemBase), null, BindingMode.TwoWay);
}
Notice that the IconUri
is of type Uri
and not string. The use of strings rather than specialized types for properties is prevalent within the Xamarin XAML VisualElements. Xamarin Forms does not have support for implicit type conversion yet. To allow the IconUri
property to be set to a string value in XAML, it is necessary to decorate the property with a TypeConverter
attribute, as demonstrated in the following excerpt:
[TypeConverter(typeof(UriTypeConverter))]
public Uri IconUri
{
get
{
return (Uri)GetValue(IconUriProperty);
}
set
{
SetValue(IconUriProperty, value);
}
}
A Tap
event can be programmatically initiated using the AppBarItemBase
class’s PerformTap
method. This allows you to pass through a tap event from the native implementations, as you see in the following sections.
The AppBarItem
class extends the AppBarItemBase
class to add commanding support. Two further bindable properties are included in the AppBarItem
class: a Command
property and a CommandParameter
property.
Commanding is optional with the AppBarItem
object. If you so choose, you can rely solely on the Tap
event to detect user actions.
When an AppBarItem
's Command
property is set, my intention was to automatically bind the AppBarItem
to the various properties of the command. Unfortunately I ran into a road block here. At the time of writing, Xamarin Forms Binding
objects do not have a RelativeSource
property, which prevents binding to the Text
and IconUri
properties of the command. See Listing 2. The method body is commented out in the repository.
Listing 2. First attempt at AppBarItem.HandleCommandChanged Method
static void BindToCommand(AppBarItem item, ICommand command)
{
if (command != null)
{
var newUICommand = command as IUICommand;
if (newUICommand != null)
{
{
if (string.IsNullOrEmpty(item.Text))
{
item.SetBinding(TextProperty, new Binding("Command.Text", BindingMode.OneWay));
}
if (!string.IsNullOrEmpty(newUICommand.IconUrl))
{
item.SetBinding(IconUriProperty, "Command.IconUrl", BindingMode.OneWay,
new StringToUriConverter());
}
item.SetBinding(IsEnabledProperty, new Binding("Command.Enabled", BindingMode.OneWay));
}
}
}
}
As a result, binding to the command's Text
property, for example, is done within the binding expression; as shown in Listing 3.
Listing 3. AboutView.xaml AppBar excerpt
<calcium:AppBar Grid.Row="1">
<calcium:AppBar.Buttons>
<calcium:AppBarItem Command="{Binding ExampleCommand}"
Text="{Binding ExampleCommand.Text}" />
</calcium:AppBar.Buttons>
</calcium:AppBar>
When a user taps the native representation of an AppBarItem
, the event 'flows through' via the AppBarItem
's overidable PerformTap
method. If a command has been assigned to the AppBarItem
, then the command’s Execute
method is called, as shown in the following excerpt:
protected override void OnTap(EventArgs e)
{
base.OnTap(e);
var command = Command;
if (command != null)
{
command.Execute(CommandParameter);
}
}
NOTE. The presence of a command does not override the Tap
event. Any subscribers are still notified of the occurrence of a tap before the command is performed.
Adapting Commands to AppBar Items
To recap, you can bind the AppBar
’s Buttons
and MenuItems
properties to IUICommand
collections. Clearly there is an intermediary step to resolve what type of IAppBarItem
to use to represent a particular IUICommand
. Enter the IAppBarItemFactory
. An IAppBarItemFactory
object’s job is to map commands to the app bar counterparts. IAppBarItemFactory
has a single method that accepts an IUICommand
and returns an IAppBarItem
. The default implementation does not do a whole lot yet, but merely creates an AppBarItem
and assigns it the specified command, as shown:
public virtual IAppBarItem BuildItem(IUICommand command)
{
ArgumentValidator.AssertNotNull(command, "command");
var result = new AppBarItem {Command = command};
return result;
}
IAppBarItemFactory
is an extensibility point because you can replace the IAppBarItemFactory
implementation to perform whatever custom mapping you’d like. The AppBar
class’s AdaptItems
method retrieves the IAppBarItemFactory
from the IoC container and falls back to the default implementation, AppBarItemFactory
, if none has been registered. See Listing 4.
Listing 4. AppBar.AdaptItems method
List<IAppBarItem> AdaptItems(IEnumerable<IUICommand> commands)
{
IAppBarItemFactory factory = Dependency.Resolve<IAppBarItemFactory, AppBarItemFactory>();
List<IAppBarItem> result = new List<IAppBarItem>();
foreach (var command in commands)
{
IAppBarItem item = factory.BuildItem(command);
result.Add(item);
}
return result;
}
In this section you how to create a custom visual element. The manner in which the AppBar
is rendered has to depend on what platform your app is running. In the next section you see how to create platform specific renderers.
Employing Platform Specific Rendering, aka Native Rendering
Let’s get into the nitty gritty of how the AppBar
is materialized on multiple platforms without the need for custom plumbing code on your part. The task of carrying out platform specific rendering is performed using the Xamarin Forms rendering APIs. In the case of Calcium’s AppBar
, it uses three different renderers: one for iOS, Android, and Windows Phone. In this section you see how each of the renderers is implemented, and you learn how to register a native renderer with the Xamarin Forms rendering system.
In Xamarin Forms, consuming a platform specific control is achieved with a custom ViewRenderer<TElement, TNativeElement>
implementation, where TElement
is the type of the Xamarin Forms custom view and TNativeElement
is the type of the platform specific element, such as a Windows Phone custom control.
NOTE: Though the naming of the generic arguments of the ViewRenderer
imply that a VisualElement
is sufficient, the type constraints of the ViewRenderer
require a class derived from Xamarin.Forms.View
.
Displaying a Windows Phone Application Bar using a View Renderer
The built-in Windows Phone application bar is one of the key components on Windows Phone. It’s used by most XAML based apps, and gives uniformity to a key aspect of the user experience. It does, however, have some challengers associated with it, if you desire any type of non-standard behaviour. For example, the Windows Phone 8 ApplicationBar
does not support data binding. Unlike the rest of the rich controls available out-of-the-box in Windows Phone Silverlight apps, it is clunky to work with and serves more or less as a shim for the underlying native control. One particular limitation is that it doesn’t support creating multiple application bars for Pivot
or Panorama
items.
Some time ago I decided to create a wrapper for the built-in application bar, which I have used in a number of commercial apps. It has proved to be a more than useful addition to my toolbox. I have leveraged the features of this custom application bar in Xamarin Forms to provide the same multi-bar support for Xamarin Page
s that are placed in a MultiPage
container such as a TabbedPage
.
The custom Windows Phone AppBar
class is rendered by the Windows Phone specific AppBarRenderer
class. You can find the AppBarRenderer
class for Windows Phone in the in the Outcoder.Calcium.XamarinForms.WindowsPhone project of the Calcium source repository. The class exists in the Outcoder.UI.Xaml.Renderers namespace.
public class AppBarRenderer : ViewRenderer<Outcoder.UI.Xaml.AppBar, Outcoder.UI.Xaml.Controls.AppBar>
{ ... }
When creating a custom renderer there are two main things you need to do. The first is that you should react to the ElementChanged
event by overriding the OnElementChanged
method of the ViewRenderer<TView,TNative>
class. When the OnElementChanged
method is called it provides the opportunity to initialize the control. The second is that once the native control has been initialized it is supplied to the renderer via the render's SetNativeControl
method.
In Listing 5 you see that the method takes care of detaching the old AppBar
(unsubscribing to its events) and attaching the new AppBar
instance. In addition, the current Xamarin Page instance is located using the custom GetHostPage
method. The renderer subscribes to the page’s Disappearing
and Appearing
events to know when to hide and rebuild the AppBar
respectively. The SetNativeControl
method gives the Xamarin Forms infrastructure the element to display within the platform specific interface.
Listing 5. Overriding the OnElementChanged
method for the Windows Phone AppBar
protected override void OnElementChanged(ElementChangedEventArgs<AppBar> e)
{
base.OnElementChanged(e);
var oldElement = e.OldElement;
if (oldElement != null)
{
DetachAppBar(oldElement);
}
if (!initialized)
{
initialized = true;
Page page = GetHostPage();
page.Disappearing += HandlePageDisappearing;
page.Appearing += HandlePageAppearing;
var wpAppBar = new Outcoder.UI.Xaml.Controls.AppBar();
SetNativeControl(wpAppBar);
}
AppBar newAppBar = e.NewElement;
AttachAppBar(newAppBar);
}
The AppBarRenderer
keeps a weak reference to the current Page to detect when another page takes ownership of the application bar. The following excerpt shows the HandlePageAppearing
method, which creates a new WeakReference
object:
void HandlePageAppearing(object sender, EventArgs e)
{
Page ownerPage = GetHostPage();
appBarOwner = new WeakReference<Page>(ownerPage);
UpdateAppBar();
}
Conversely, when the user navigates away from a page, the HandlePageDisappearing
method is called. If the new page does not correspond to the page that was registered when the page appeared, then it means that a different page now owns the application bar. The reason for tracking the active page is that the Appearing
event of a new page is raised prior to the Disappearing
event of the current page.
void HandlePageDisappearing(object sender, EventArgs e)
{
Page ownerPage;
if (appBarOwner != null && appBarOwner.TryGetTarget(out ownerPage))
{
var hostContentPage = GetHostPage();
if (ownerPage != hostContentPage)
{
return;
}
}
HideAppBar();
}
Retrieving the host page is achieved by taking a stroll up through the visual tree; calling the custom method GetParentOfType<Page>()
. See Listing 6. I intend on incorporating this visual tree logic into the Calcium base library, akin to the visual tree helper logic you see in Calcium’s WPF and Windows Phone specific core.
Listing 6. AppBarRenderer.GetParentOfType
T GetParentOfType<T>() where T : class
{
T page = null;
Element parent = Element;
while (page == null)
{
parent = parent.Parent;
if (parent == null)
{
break;
}
page = parent as T;
}
return page;
}
When a new AppBar
is presented to the renderer, the AttachAppBar
method subscribes to the AppBar
’s ButtonCollectionChanged
and MenuItemCollectionChanged
events. See Listing 7.
Listing 7. Windows Phone AppBarRenderer.AttachAppBar Method
void AttachAppBar(AppBar appBar)
{
if (appBar == null)
{
return;
}
DetachAppBar(appBar);
appBar.ButtonCollectionChanged += HandleButtonsChanged;
appBar.MenuItemCollectionChanged += HandleMenuItemsChanged;
}
When a button or menu item is added or removed, the AppBar
is rebuilt via a call to the renderer’s UpdateAppBar
method. See Listing 8. Use the Control
property of the base ViewRenderer
to retrieve the native control that you supplied earlier.
If the AppBar
for Xamarin Forms contains menu items, but no buttons, then the Windows Phone application bar is set to a minimized state; reducing its size while still allowing the menu to be expanded.
A new item for the native AppBar
is created for each item in the AppBar
for Xamarin Forms. The AppBarRenderer
assigns a handler to the Click event of each native item, which passes on the event to the PerformTap
method of the IAppBarItem
.
You may notice that more work needs to be done to respond to changes in the AppBarItem
objects. I have plans to make property changes from the Xamarin Forms IAppBarItem
objects flow through to the native AppBar
items.
Listing 8. Windows Phone AppBarRenderer.UpdateAppBar Method
void UpdateAppBar()
{
var wpAppBar = Control;
if (wpAppBar == null)
{
return;
}
wpAppBar.Buttons.Clear();
wpAppBar.MenuItems.Clear();
var xamAppBar = Element;
var xamButtons = xamAppBar.Buttons.ToList();
var xamMenuItems = xamAppBar.MenuItems.ToList();
if (!xamButtons.Any())
{
if (!xamMenuItems.Any())
{
wpAppBar.IsVisible = false;
return;
}
wpAppBar.Mode = Microsoft.Phone.Shell.ApplicationBarMode.Minimized;
}
wpAppBar.IsVisible = true;
foreach (var item in xamAppBar.Buttons)
{
var button = new Outcoder.UI.Xaml.Controls.AppBarIconButton();
button.Text = item.Text;
button.IconUri = item.IconUri;
IAppBarItem i = item;
button.Click += (sender, args) => i.PerformTap();
wpAppBar.Buttons.Add(button);
}
foreach (IAppBarItem item in xamAppBar.MenuItems)
{
var menuItem = new Outcoder.UI.Xaml.Controls.AppBarMenuItem();
menuItem.Text = item.Text;
IAppBarItem i = item;
menuItem.Click += (sender, args) => i.PerformTap();
wpAppBar.MenuItems.Add(menuItem);
}
}
Using a View Renderer to Display a Toolbar in iOS
As an aside, when I began working on building a cross-platform AppBar
, the Xamarin APIs were rather different. The developers at Xamarin did some refactoring to unify the rendering system across the three platforms; not a bad thing.
My initial intention was to leverage the platform specific APIs for populating a toolbar on iOS and Android. It turned out, however, that my code and Xamarin’s began to step on each other’s toes. You see, the way you create a toolbar in Xamarin Forms is by populating the ToolbarItems
property of a Xamarin Forms Page. The Page
renderer then constructs the toolbar when it is building up the page. This interfered with my AppBar
. I changed tack and rather than build-up toolbars manually, I instead decided to leverage the Xamarin Forms ToolbarItems
property; populating it according to the content of the AppBar
.
You’ll notice that in Listing 9, the iOS AppBarRenderer
sets its native control to a label. This is because there’s no native backing control that I needed to manipulate, but I still needed to supply something to the Xamarin rendering subsystem. I’m hoping the Xamarin crew will advise me of an alternative to providing a fake control.
There are differences in how images are resolved on each platform, making it difficult to provide a unified approach for placing and locating images. I believe I’ve come up with some neat ways around this. You see a whole section devoted to this in a later article in this series. The IImageUrlTransformer
object is related to this aspect, so please disregard it for now, as you will return to it later.
The triggers for populating the toolbar are nearly identical to the Windows Phone implementation; the AppRenderer
relies on the active Page
’s Appearing
and Disappearing
events to determine when it should populate the Xamarin Forms ToolbarItems
property of the current page.
Listing 9. iOS AppBarRenderer.OnElementChanged Method
protected override void OnElementChanged(ElementChangedEventArgs<AppBar> e)
{
base.OnElementChanged(e);
var newAppBar = e.NewElement;
if (!initialized)
{
initialized = true;
imageUrlTransformer = Dependency.Resolve<IImageUrlTransformer, ImageUrlTransformer>(true);
SetNativeControl(new UILabel(RectangleF.Empty));
Page page = GetHostContentPage();
page.Disappearing += (sender, args) =>
{
DetachAppBar(newAppBar);
Page ownerPage;
if (toolbarOwner != null && toolbarOwner.TryGetTarget(out ownerPage))
{
var hostContentPage = GetHostContentPage();
if (ownerPage != hostContentPage)
{
return;
}
}
RemoveToolbar();
};
page.Appearing += (sender, args) =>
{
Page ownerPage = GetHostContentPage();
toolbarOwner = new WeakReference<Page>(ownerPage);
UpdateAppBar();
AttachAppBar(newAppBar);
};
}
}
You now look at how the AppBar
is represented using the Xamarin Forms ToolbarItems
collection. A ToolbarItem
is created for each button in the AppBar
. See Listing 10. When the Activated
event of the ToolBarItem
is raised, the event handler calls the PerformTap
method of the AppBarItem
.
A menu item collection does not exist for the toolbar. The AppBarRenderer
therefore constructs a special menu button, which is analogous to the ellipsis button on the Windows Phone application bar. When the menu button is activated, the custom DisplayMenu
method is called. We turn to that, next.
Listing 10. iOS AppRenderer. UpdateAppBar Method
void UpdateAppBar()
{
var appBar = Element;
Page page = GetParentOfType<ContentPage>();
if (page == null)
{
return;
}
List<ToolbarItem> items = null;
foreach (IAppBarItem appBarItem in appBar.Buttons)
{
string text;
if (!AppBarItemPropertyResolver.TryGetItemText(appBarItem, out text))
{
throw new Exception("Unable to resolve text for button.");
}
ToolbarItem item = new ToolbarItem { Name = text };
IAppBarItem itemForClosure = appBarItem;
item.Activated += (sender, e) => itemForClosure.PerformTap();
string iconUrl;
if (AppBarItemPropertyResolver.TryGetItemUrl(appBarItem, out iconUrl))
{
string transformedUrl = imageUrlTransformer.TransformForCurrentPlatform(iconUrl);
item.Icon = transformedUrl;
}
if (items == null)
{
items = new List<ToolbarItem>();
}
items.Add(item);
}
var menuItemsEnumerable = appBar.MenuItems;
if (menuItemsEnumerable != null)
{
var menuItems = menuItemsEnumerable.ToList();
int menuItemCount = menuItems.Count;
if (menuItemCount > 0)
{
string[] buttonTitles = new string[menuItemCount];
for (int i = 0; i < menuItemCount; i++)
{
string text;
if (!AppBarItemPropertyResolver.TryGetItemText(menuItems[i], out text))
{
throw new Exception("Unable to resolve text for menu item.");
}
buttonTitles[i] = text;
}
ToolbarItem item = new ToolbarItem { Name = menuToolbarItemText };
item.Activated += async (sender, e) =>
{
var viewController = UIApplication.SharedApplication.Windows[0].RootViewController;
var uiView = viewController.View;
var action = await DisplayMenu(uiView, "Cancel", buttonTitles);
if (action > -1)
{
var selectedItem = menuItems[action];
selectedItem.PerformTap();
}
};
if (items == null)
{
items = new List<ToolbarItem>();
}
items.Add(item);
}
}
if (items != null && items.Any())
{
var toolbarItems = page.ToolbarItems;
toolbarItems.Clear();
items.Reverse();
toolbarItems.AddRange(items);
}
}
The AppBarItemPropertyResolver
class is used to retrieve the text and icon URL for the AppBarItem
. It does so by first attempting to retrieve the property directly from the AppBarItem
. If null, then the property is retrieve from the AppBarItem
's command. See Listing 11.
Listing 11. AppBarItemPropertyResolver.TryGetItemText method.
internal static bool TryGetItemText(IAppBarItem appBarItem, out string text)
{
bool resolvedText = false;
text = appBarItem.Text;
if (!string.IsNullOrEmpty(text))
{
resolvedText = true;
}
else
{
var commandItem = appBarItem as AppBarItem;
if (commandItem != null && commandItem.Command != null)
{
var uiCommand = commandItem.Command as IUICommand;
if (uiCommand != null)
{
text = uiCommand.Text;
resolvedText = true;
}
}
}
return resolvedText;
}
Displaying a Custom Menu in Xamarin.iOS
The iOS AppBarRenderer
relies on the UIActionSheet
class to display a list of options. Buttons are added to the UIActionSheet
object using its AddButton
method. See Listing 12.
The DisplayMenu
method is awaitable, and the UI thread is not blocked while the UIActionSheet
is being displayed. A TaskCompletionSource<int>
is used to await closure of the UIActionSheet
and the result int value is the index of the button that is tapped, or -1 if it is cancelled.
Listing 12. iOS AppBarRenderer.DisplayMenu Method
Task<int> DisplayMenu(UIView uiView, string cancelText, params string[] buttons)
{
var sheet = new UIActionSheet();
foreach (var button in buttons)
{
sheet.AddButton(button);
}
int cancelButtonIndex = sheet.ButtonCount;
sheet.AddButton(cancelText);
sheet.CancelButtonIndex = cancelButtonIndex;
TaskCompletionSource<int> source = new TaskCompletionSource<int>();
try
{
sheet.Clicked += delegate(object sender, UIButtonEventArgs args)
{
int result;
if (args != null)
{
int buttonIndex = args.ButtonIndex;
if (buttonIndex != cancelButtonIndex)
{
result = buttonIndex;
}
else
{
result = -1;
}
}
else
{
result = -1;
}
source.SetResult(result);
};
sheet.ShowInView(uiView);
}
catch (Exception ex)
{
source.SetException(ex);
}
return source.Task;
}
The UIActionSheet
is displayed when the user taps the ellipsis button in our custom AppBar
. See Figure 4. The Tap
event of each AppBarItem
is raised and if there is a command associated with the AppBarItem
it is performed.
Figure 4. Displaying a Menu using the UIActionSheet
.
Using a View Renderer to Display an Android Toolbar
In this final part of the overview of the AppBar
view renderers, you look at the AppBar
ViewRenderer
implementation for Android.
The AppBarRenderer
for Android is located in the Outcoder.Calcium.Android project in the Calcium source code repository. It closely resembles the iOS AppBarRenderer
in that it also leverages the Xamarin Forms ToolbarItems
property of the active page. So, there’s very little native activity that takes place. The key difference is in the way the menu is displayed to the user. See Listing 13.
The Calcium ActionDialog
is used to present a list of options to the user. The DisplayMenu
method is awaitable, and returns a Task<int>
. The result of the method is the index of the button that was tapped.
Listing 13. Android AppBarRenderer.DisplayMenu Method
Task<int> DisplayMenu(params string[] buttons)
{
ActionDialogArguments arguments = new ActionDialogArguments(null, null, null, buttons);
ActionDialog dialog = new ActionDialog(arguments, Context);
dialog.Show();
return arguments.Result.Task;
}
ActionDialog
resembles the Xamarin.Android ActionSheet
class. There are, however, a couple notable difference. Calcium's ActionDialog
uses an index based system rather than a string based system to identify which button was tapped. It also allows the title of the dialog to be hidden. Something that I thought made sense with the AppBar
scenario. It does this by calling the base Dialog
class’s RequestWindowFeature
, as shown in the following excerpt from the ActionDialog
’s OnCreate
method:
protected override void OnCreate(Bundle savedInstanceState)
{
string actionSheetTitle = arguments.ActionSheetTitle;
bool showTitle = !string.IsNullOrWhiteSpace(actionSheetTitle);
if (!showTitle)
{
RequestWindowFeature((int)WindowFeatures.NoTitle);
}
base.OnCreate(savedInstanceState);
…
}
NOTE. In the event that you create your own custom Android Dialog, the RequestWindowFeature
call needs to be made before the base.OnCreate
method call.
I’ll not detail the rest of the ActionDialog
code here. If you are interested in that, you can find the ActionDialog
class in the Outcoder.Calcium.Android project in the Calcium source code repository.
Tapping the ellipsis button on Android presents the menu items. See Figure 5.
Figure 5. AppBar menu in Android
Registering a View Renderer
To register a Xamarin Forms native renderer you use an ExportRenderer
attribute, like so:
[assembly: ExportRenderer(typeof(Outcoder.UI.Xaml.AppBar),
typeof(Outcoder.UI.Xaml.Renderers.AppBarRenderer))]
At the time of writing there is a platform disparity when exporting renderers. Xamarin.Android and Xamarin.iOS projects both allow you to export a renderer from within a class library. This is not the case with Windows Phone. To use the AppBar
in a Windows Phone project you must explicitly add an ExportRenderer
attribute to your code. In the sample, I chose to do this within the MainPage.xaml.cs file in the Windows Phone project.
Conclusion
In this article you looked at defining a custom AppBar
in XAML; binding it to collections of command objects and at directly binding AppBar
items to viewmodel properties. You saw how to employ platform specific rendering to display an AppBar
on iOS, Android, and Windows Phone. You learned how the Calcium AppBar
is multi-page aware and can be placed within multiple pages hosted in a CarouselPage or a TabbedPage. You saw how to display a custom menu in iOS and Android. Finally, you looked at how to register a custom view renderer.
In the next article you learn how to place images anywhere in a shared project as Content resources like in Windows Phone, and consume them in the same way across all three platforms.
I hope you find this project useful. If so, then I'd appreciate it if you would rate it and/or leave feedback below. This will help me to make my next article better.
History
October 2014