Home

Awesome

Latest

.NET 8 support. Nuget here

Use package 8.0.0 and above if you are building against .NET 8.
Use package 2.3.4-pre2 if you are building against .NET 7.
Use package 2.0.0 if you are building against .NET 6.

Workarounds

If you're having trouble with the MAUI TabbedPage or FlyoutPage see below

Controls

NuGet package

  1. ListViewZero
  2. TreeViewZero
  3. MaskViewZero

ListViewZero

Features

If you can use a CollectionView or a ListView you will have no trouble with a ListViewZero

TODO: Sample image

ListViewZero exposes the following properties

PropertyTypeBindablePurpose
ItemContainerStyleStyleYesAn optional Style that can be applied to the ListItemZero instances that represent each node. This can be used to modify how selected-items are rendered.
ItemHeightfloatYesThe height of each row in the list-view
ItemsSourceobjectYesSet this to the IEnumerable (usually found in your view-model) that contains your items
ItemTemplateDataTemplateYesUsed to draw the data for each node. Set this to a DataTemplate or a DataTemplateSelector. See below.
ScrollOffsetfloatYES!This is the absolute offset and can bound to.
SelectedItemobjectYesSet to the currently selected item, i.e. an instance of your ViewModel data, or null
SelectedItemsIListYesAll currently selected items. Default is an ObservableCollection<object>. You can bind to it or set your own collection, and if it supports INotifyCollectionChanged the ListViewZero will track it.
SelectionModeSelectionModeYesAllows a SelectionMode of None, Single or Multiple.
RemainingItemsintYesThis tracks the number of items in the ItemsSource that are below the bottom of the ListViewZero.
RemainingItemsChangedCommandICommandYesThis is raised whenever RemainingItems changes. The command parameter is set to RemainingItems.

Create a ListViewZero

Given a collection of items

public IEnumerable<Person> ListData { get; }

Add the namespace:

xmlns:cz="clr-namespace:FunctionZero.Maui.Controls;assembly=FunctionZero.Maui.Controls"

Then declare a ListViewZero like this:

<!--Tip: A generous ItemHeight ensures the items aren't too small to tap with your finger-->
<cz:ListViewZero 
    ItemsSource="{Binding SampleListData}"
    ItemHeight="40"

    ... the rest are optional ...

    SelectedItem="{Binding SelectedItem}"
    SelectedItems="{Binding SelectedItems}"
    SelectionMode="Multiple"
    >
    <cz:ListViewZero.ItemTemplate>
        <DataTemplate>
            <Label Text="{Binding Name}" />
        </DataTemplate>
    </cz:ListViewZero.ItemTemplate>
</cz:ListViewZero>

Tracking changes in the data

If the ItemsSource supports INotifyCollectionChanged, the list-view will track all changes automatically. E.g.

public ObservableCollection<Person> ListData { get; }

If the properties on your items support INotifyPropertyChanged then they too will be tracked.

For example, ListViewZero will track changes to Name property on the following node:

public class Person : BaseClassWithInpc
{
   private string _name;
   public string Name
   {
      get => _name;
      set => SetProperty(ref _name, value);
   }
}

SelectionMode

Similar to the CollectionView, allowed values are None, Single or Multiple. You can change this property at runtime, e.g. via Binding

SelectedItem / SelectedItems

SelectedItem tracks the currently selected item, and can be databound to your ViewModel

SelectedItems defaults to an ObservableCollection and tracks all items whose IsSelected property is true. The default BindingMode is TwoWay
In your view-model you can bind to the default collection (BindingMode OneWayToSource) or replace it with your own collection (BindingMode OneWay or TwoWay)
The ListViewZero will maintain the contents of the collection for you, and you can modify the collection from your view-model to programatically select items

Styling SelectedItems

You can replace this styling by setting the ItemContainerStyle property on your ListViewZero
Selected items are rendered using a VisualStateManager and 3 of the 4 CommonStates
Selected items are rendered using a VisualStateManager and the following states

Common StateDescriptionIsSelectedIsPrimarySelectionMode
NormalThe ListViewItem is not selectedFalseFalseAny
ItemFocusedThe ListViewItem is the primary-selectionTrueTrueSingle or Multiple
SelectedThe ListViewItem is selected but not the primaryTrueFalseMultiple
DisabledNot usedn/an/an/a

This is the default Style used to modify the BackgroundColor of selected items, and can serve as a baseline for your own

<Style x:Key="testItemStyle" TargetType="cz:ListItemZero">
    <Setter Property="VisualStateManager.VisualStateGroups" >
        <VisualStateGroupList>
            <VisualStateGroup x:Name="CommonStates">

                <!-- BackgroundColor must have a value set or the other states cannot 'put back' the original color -->
                <!-- I *think* this is due to a bug in MAUI because unappyling a Setter ought to revert to the original value or default -->
                <VisualState x:Name="Normal" >
                    <VisualState.Setters>
                 <Setter Property="BackgroundColor" Value="Transparent" />
                    </VisualState.Setters>
                </VisualState>

                <VisualState x:Name="ItemFocused">
                    <VisualState.Setters>
                        <Setter Property="BackgroundColor" Value="Cyan" />
                    </VisualState.Setters>
                </VisualState>

                <VisualState x:Name="Selected">
                    <VisualState.Setters>
                        <Setter Property="BackgroundColor" Value="AliceBlue" />
                    </VisualState.Setters>
                </VisualState>

            </VisualStateGroup>
        </VisualStateGroupList>
    </Setter>
</Style>

And set it like this:

<cz:ListViewZero 
    SelectionMode="Multiple"
    ItemContainerStyle="{StaticResource testItemStyle}"
    ...

TreeViewZero

Sample image

This control allows you to visualise a tree of any data. Each trunk node must provide its children using a public property that supports the IEnumerable interface.
If the children are in a collection that supports INotifyCollectionChanged the control will track changes to the underlying tree data.
Children are lazy-loaded and the UI is virtualised.

TreeViewZero exposes the following properties

PropertyTypeBindablePurpose
IndentMultiplierdoubleYes (OneTime)How far the TreeNode should be indented for each nest level. Default is 15.0
IsRootVisibleboolYesSpecifies whether the root node should be shown or omitted.
ItemContainerStyleStyleYesAn optional Style that can be applied to the ListItemZero instances that represent each node. This can be used to modify how selected-items are rendered.
ItemHeightfloatYesThe height of each row in the tree-view
ItemsSourceobjectYesSet this to your root node
ScrollOffsetfloatYES!This is the absolute offset and can bound to
SelectedItemobjectYesSet to the currently selected item, i.e. an instance of your ViewModel data, or null
SelectedItemsIListYesAll currently selected items. Default is an ObservableCollection<object>. You can bind to it or set your own collection, and if it supports INotifyCollectionChanged the TreeViewZero will track it.
SelectionModeSelectionModeYesAlloows a SelectionMode of None, Single or Multiple.
TreeChildrenIEnumerableNoThis is exposed for future capabilities and exposes all items potentially visible in the viewport.
TreeItemControlTemplateControlTemplateYesAlows you to replace the default ControlTemplate used to render each node
TreeItemTemplateTemplateProviderYesUsed to draw the data for each node. Set this to a TreeItemDataTemplate or a TreeItemDataTemplateSelector. See below.

TreeItemDataTemplate

TreeItemDataTemplate tells a tree-node how to draw its content, how to get its children and whether it should bind IsExpanded to the underlying data.
It declares the following properties:

PropertyTypePurpose
ChildrenPropertyNamestringThe name of the property used to find the node children
IsExpandedPropertyNamestringThe name of the property used to store whether the node is expanded
ItemTemplateDataTemplateThe DataTemplate used to draw this node
TargetTypeTypeWhen used in a TreeItemDataTemplateSelector, identifies the least-derived nodes the ItemTemplate can be applied to.

Create a TreeViewZero

Given a hierarchy of MyNode

public class MyNode
{
   public string Name { get; set;}
   public IEnumerable<MyNode> MyNodeChildren { get; set; }
}

Add the namespace:

xmlns:cz="clr-namespace:FunctionZero.Maui.Controls;assembly=FunctionZero.Maui.Controls"

Then declare a TreeViewZero like this:

<!--Tip: A generous ItemHeight ensures the chevrons aren't too small to tap with your finger-->
<cz:TreeViewZero ItemsSource="{Binding RootNode}" ItemHeight="100">
    <cz:TreeViewZero.TreeItemTemplate>
        <cz:TreeItemDataTemplate ChildrenPropertyName="MyNodeChildren">
            <DataTemplate>
                <Label Text="{Binding Name}" />
            </DataTemplate>
        </cz:TreeItemDataTemplate>
    </cz:TreeViewZero.TreeItemTemplate>
</cz:TreeViewZero>

Tracking changes in the data

If the children of a node support INotifyCollectionChanged, the TreeView will track all changes automatically.
If the properties on your node support INotifyPropertyChanged then they too will be tracked.

For example, TreeViewZero will track changes to Name, IsExpanded and any modifications to the Children collection on the following node:

public class MyObservableNode : BaseClassWithInpc
{
   private string _name;
   public string Name
   {
      get => _name;
      set => SetProperty(ref _name, value);
   }

   private bool _isMyNodeExpanded;
   public bool IsMyNodeExpanded
   {
      get => _isMyNodeExpanded;
      set => SetProperty(ref _isMyNodeExpanded, value);
   }

   public ObservableCollection<MyObservableNode> Children {get; set;}
}

This is how to bind the IsMyNodeExpanded from our data, to IsExpanded on the TreeNode ...

<cz:TreeViewZero.TreeItemTemplate>
    <cz:TreeItemDataTemplate ChildrenPropertyName="Children" IsExpandedPropertyName="IsMyNodeExpanded">
        <DataTemplate>
            ...
        </DataTemplate>
    </cz:TreeItemDataTemplate>
</cz:TreeViewZero.TreeItemTemplate>

SelectionMode

The TreeViewZero allows selection modes None, Single or Multiple. Please see the ListViewZero docs for how to use the SelectionMode property.

Styling SelectedItems

Use this to style each tree-node, e.g. to change how selected items are rendered.
See Styling SelectedItems on the ListViewZero for details, or use the following as a guide:

<Style x:Key="testItemStyle" TargetType="cz:ListItemZero">
    <Setter Property="VisualStateManager.VisualStateGroups" >
        <VisualStateGroupList>
            <VisualStateGroup x:Name="CommonStates">

                <!-- BackgroundColor must have a value set or the other states cannot 'put back' the original color -->
                <!-- I *think* this is due to a bug in MAUI because unappyling a Setter ought to revert to the original value or default -->
                <VisualState x:Name="Normal" >
                    <VisualState.Setters>
                 <Setter Property="BackgroundColor" Value="Transparent" />
                    </VisualState.Setters>
                </VisualState>

                <VisualState x:Name="ItemFocused">
                    <VisualState.Setters>
                        <Setter Property="BackgroundColor" Value="Cyan" />
                    </VisualState.Setters>
                </VisualState>

                <VisualState x:Name="Selected">
                    <VisualState.Setters>
                        <Setter Property="BackgroundColor" Value="AliceBlue" />
                    </VisualState.Setters>
                </VisualState>

            </VisualStateGroup>
        </VisualStateGroupList>
    </Setter>
</Style>

And set it like this:

<cz:TreeViewZero 
    SelectionMode="Multiple"
    ItemContainerStyle="{StaticResource testItemStyle}"

TreeItemDataTemplateSelector

If your tree of data consists of disparate nodes with different properties for their Children, use a TreeItemDataTemplateSelector and set TargetType for each TreeItemDataTemplate.

Note: In this example, the tree data can contain nodes of type LevelZero, LevelOne and LevelTwo where each type has a different property to provide its children.

The first TargetType your data-node can be assigned to is used. Put another way, the first TargetType the data-node can be cast to, wins.

<cz:TreeViewZero ItemsSource="{Binding SampleTemplateTestData}" ItemHeight="20" >
    <cz:TreeViewZero.TreeItemTemplate>
        <cz:TreeItemDataTemplateSelector>
            <cz:TreeItemDataTemplate ChildrenPropertyName="LevelZeroChildren" TargetType="{x:Type test:LevelZero}" IsExpandedPropertyName="IsLevelZeroExpanded">
                <DataTemplate>
                    <Label Text="{Binding Name}" BackgroundColor="Yellow" />
                </DataTemplate>
            </cz:TreeItemDataTemplate>

            <cz:TreeItemDataTemplate ChildrenPropertyName="LevelOneChildren" TargetType="{x:Type test:LevelOne}" IsExpandedPropertyName="IsLevelOneExpanded">
                <DataTemplate>
                    <Label Text="{Binding Name}" BackgroundColor="Cyan" />
                </DataTemplate>
            </cz:TreeItemDataTemplate>

            <cz:TreeItemDataTemplate ChildrenPropertyName="LevelTwoChildren" TargetType="{x:Type test:LevelTwo}" IsExpandedPropertyName="IsLevelTwoExpanded">
                <DataTemplate>
                    <Label Text="{Binding Name}" BackgroundColor="Pink" />
                </DataTemplate>
            </cz:TreeItemDataTemplate>

            <cz:TreeItemDataTemplate ChildrenPropertyName="LevelThreeChildren" TargetType="{x:Type test:LevelThree}" IsExpandedPropertyName="IsLevelThreeExpanded">
                <DataTemplate>
                    <Label Text="{Binding Name}" BackgroundColor="Crimson" />
                </DataTemplate>
            </cz:TreeItemDataTemplate>
        </cz:TreeItemDataTemplateSelector>
    </cz:TreeViewZero.TreeItemTemplate>
</cz:TreeViewZero>

Customising TreeItemDataTemplateSelector

If you want full-control over the TreeItemTemplate per node, you can easily implement your own TreeItemDataTemplateSelector and override OnSelectTemplateProvider. Here's an example that chooses a template based on whether the node has children or not:

public class MyTreeItemDataTemplateSelector : TemplateProvider
{
    /// These should be set in the xaml markup. (or code-behind, if that's how you roll)
    public TreeItemDataTemplate TrunkTemplate{ get; set; }
    public TreeItemDataTemplate LeafTemplate{ get; set; }

    public override TreeItemDataTemplate OnSelectTemplateProvider(object item)
    {
        if(item is MyTreeNode mtn)
            if((mtn.Children != null) && (mtn.Children.Count != 0))
                return TrunkTemplate;
        
        return LeafTemplate;
    }
}

Take a look at TreeItemDataTemplateSelector.cs for an example of how to provide a collection of TreeItemDataTemplate instances to your TemplateProvider.

Drawing your own tree-nodes

Do this if you want to change the way the whole tree-node is drawn, e.g. to replace the chevron. It is a two-step process.

  1. Create a ControlTemplate for the node
  2. Apply it to the TreeViewZero

The templated parent for the ControlTemplate is a ListItemZero. It exposes these properties:

PropertyTypePurpose
IsPrimaryboolIf selection is allowed, this tracks the current SelectedItem
IsSelectedboolIf the current item is selected, this is true. Note we can have multiple items selected, but only one SelectedItem
ItemIndexintFor internal use when managing the cache
ItemTemplateDataTemplateThe DataTemplate used to generate the ListViewItem Content

The BindingContext of the templated parent is a TreeNodeContainer and includes the following properties:

PropertyTypePurpose
IndentintHow deep the node should be indented. It is equal to NestLevel, or NestLevel-1 if the Tree Root is not shown.
NestLevelintThe depth of the node in the data.
IsExpandedboolThis property reflects whether the TreeNode is expanded.
ShowChevronboolWhether the chevron is drawn. True if the node has children.
DataobjectThis is the tree-node data for this TreeNodeZero instance, i.e. your data!

Step 1 - Create a ControlTemplate ...

You can base the ControlTemplate on the default, show here, or bake your own entirely.

<ControlTemplate x:Key="defaultControlTemplate">
    <HorizontalStackLayout HeightRequest="{Binding Height, Mode=OneWay, Source={x:Reference tcp}}" >
        <!--
        The ControlTemplate sets the TreeNodeSpacer BindingContext to "{Binding Source={RelativeSource TemplatedParent}}" for us
        i.e. a ListItemZero.
        The TreeNodeSpacer walks up the visual-tree to find the containing TreeViewZero, to get the IndentMultiplier.
        It then sets its WidthRequest to the IndentMultiplier * (ListItemZero.BindingContext.Indent - 1)
        -->
        <cz:TreeNodeSpacer />

        <cz:Chevron
            IsExpanded="{TemplateBinding BindingContext.IsExpanded, Mode=TwoWay}" 
            ShowChevron="{TemplateBinding BindingContext.ShowChevron, Mode=TwoWay}" 
            />
        <!--This is simply a ContentPresenter that allows us to specify a BindingContext for the Content-->
        <cz:TreeContentPresenter 
            VerticalOptions="Fill" 
            x:Name="tcp" 
            HorizontalOptions="Fill"   
            BindingContext="{TemplateBinding BindingContext.Data, Mode=OneWay}"
            />
    </HorizontalStackLayout>
</ControlTemplate>

Step 2 - give it to the TreeView ...

<cz:TreeViewZero
    ItemsSource="{Binding SampleData}" 
    IndentMultiplier="20" 
    TreeItemControlTemplate="{StaticResource MyTreeItemControlTemplate}"
    ItemHeight="60"
    >
    <cz:TreeViewZero.TreeItemTemplate>
        <cz:TreeItemDataTemplate ChildrenPropertyName="Children" IsExpandedPropertyName="IsDataExpanded">
            <DataTemplate>
                <Label Text="{Binding Name}" BackgroundColor="Pink" />
            </DataTemplate>
        </cz:TreeItemDataTemplate>
    </cz:TreeViewZero.TreeItemTemplate>
</cz:TreeViewZero>

MaskViewZero

There's a cool new control for masking out areas of the screen.
It's really boring writing documentation so here's a quick sample whilst I finish the control off.

Put your UI inside a MaskZero control, e.g. using a ControlTemplate ...

<ContentPage.ControlTemplate>
    <ControlTemplate>
        <cz:MaskZero 
            BackgroundAlpha="0.5" 
            MaskTargetName="{TemplateBinding BindingContext.TargetName}"
            MovementEasing="{x:Static Easing.CubicInOut}"
            MaskRoundnessEasing="{x:Static Easing.CubicInOut}"
            Duration="450"
            MaskColorRequest="{TemplateBinding BindingContext.MaskColor}"
            MaskEdgeColorRequest="{TemplateBinding BindingContext.MaskEdgeColor}"
            >
            <cz:MaskZero.Content>
                <ContentPresenter/>
            </cz:MaskZero.Content>
        </cz:MaskZero>
    </ControlTemplate>
</ContentPage.ControlTemplate>

Notice we are binding to the control's ~Request properties. This means any changes will be animated, using the Easing functions you provide.

Now give some of your controls a MaskZero.MaskName

<Grid xmlns:cz="clr-namespace:FunctionZero.Maui.Controls;assembly=FunctionZero.Maui.Controls"
      RowDefinitions="*,*" ColumnDefinitions="*,*" >
    <Label Grid.Row="0" Grid.Column="0" Text="Banana!"     cz:MaskZero.MaskName="banana" />
    <Label Grid.Row="0" Grid.Column="1" Text="Radish!"     cz:MaskZero.MaskName="radish" />
    <Label Grid.Row="1" Grid.Column="0" Text="Melon!"      cz:MaskZero.MaskName="melon" />
    <Label Grid.Row="1" Grid.Column="1" Text="Grapefruit!" cz:MaskZero.MaskName="grapefruit" />
</Grid>

Finally, add to your ViewModel the properties the ControlTemplate binds to, and set them, simple as that!

private async Task DoTheThingAsync()
{
    while (true)
    {
        await Task.Delay(2000);

        TargetName = "banana";
        MaskColor = Colors.Red;
        MaskEdgeColor = Colors.Black;
        await Task.Delay(2000);

        TargetName = "radish";
        MaskColor = Colors.Purple;
        MaskEdgeColor = Colors.Black;
        await Task.Delay(2000);

        TargetName = "melon";
        MaskColor = Colors.Blue;
        MaskEdgeColor = Colors.Red;
        await Task.Delay(2000);

        TargetName = "grapefruit";
        MaskColor = Colors.Yellow;
        MaskEdgeColor = Colors.Black;
    }
}

Run the demo to see different controls highlighted, with animated color, shape and opacity changes. Code is here:

Workarounds:

AdaptedTabbedPage MAUI bug 14572

Update:

AdaptedFlyoutPage MAUI bug 13496