Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/Keflon/FunctionZero.Maui.Controls

Virtualising TreeView and ListView
https://github.com/Keflon/FunctionZero.Maui.Controls

csharp

Last synced: 30 days ago
JSON representation

Virtualising TreeView and ListView

Awesome Lists containing this project

README

        

# Latest
### .NET 8 support. Nuget [here](https://www.nuget.org/packages/FunctionZero.Maui.Controls)
Use package [8.0.0](https://www.nuget.org/packages/FunctionZero.Maui.Controls/8.0.0-pre1) and above if you are building against .NET 8.
Use package [2.3.4-pre2](https://www.nuget.org/packages/FunctionZero.Maui.Controls/2.3.4-pre2) if you are building against .NET 7.
Use package [2.0.0](https://www.nuget.org/packages/FunctionZero.Maui.Controls/2.0.0) if you are building against .NET 6.
# Workarounds
If you're having trouble with the MAUI `TabbedPage` or `FlyoutPage` see [below](#workarounds)

# Controls
[NuGet package](https://www.nuget.org/packages/FunctionZero.Maui.Controls)

1. [ListViewZero](#listviewzero)
1. [TreeViewZero](#treeviewzero)
1. [MaskViewZero](#maskviewzero)

## ListViewZero
### Features
- A fully virtualising list-view that doesn't [leak memory](https://github.com/dotnet/maui/issues/8151) or [enforce arbitrary item spacing](https://github.com/dotnet/maui/issues/4520).
- Very high performance
- All rendering uses cross-platform code

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
Property | Type | Bindable | Purpose
:----- | :---- | :----: | :-----
ItemContainerStyle | Style | Yes | An 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.
ItemHeight | float | Yes | The height of each row in the list-view
ItemsSource | object | Yes | Set this to the IEnumerable (usually found in your view-model) that contains your items
ItemTemplate | DataTemplate | Yes | Used to draw the data for each node. Set this to a `DataTemplate` or a `DataTemplateSelector`. See below.
ScrollOffset | float | YES! | This is the absolute offset and can bound to.
SelectedItem | object | Yes | Set to the currently selected item, i.e. an instance of your *ViewModel* data, or null
SelectedItems | IList | Yes | All currently selected items. Default is an `ObservableCollection`. You can bind to it or set your own collection, and if it supports `INotifyCollectionChanged` the `ListViewZero` will track it.
SelectionMode | SelectionMode | Yes | Allows a `SelectionMode` of None, Single or Multiple.
RemainingItems | int | Yes | This tracks the number of items in the `ItemsSource` that are below the bottom of the `ListViewZero`.
RemainingItemsChangedCommand | ICommand | Yes | This is raised whenever `RemainingItems` changes. The _command parameter_ is set to `RemainingItems`.

### Create a ListViewZero
Given a collection of items
```csharp
public IEnumerable ListData { get; }
```
Add the namespace:
```xml
xmlns:cz="clr-namespace:FunctionZero.Maui.Controls;assembly=FunctionZero.Maui.Controls"
```
Then declare a `ListViewZero` like this:
```xml





```

### Tracking changes in the data
If the ItemsSource supports `INotifyCollectionChanged`, the list-view will track all changes automatically. E.g.
```csharp
public ObservableCollection 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:
```csharp
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 State | Description | IsSelected | IsPrimary | SelectionMode
:----- | :---- | :---- | :---- | :----
Normal | The ListViewItem is not selected | False | False | Any
ItemFocused | The ListViewItem is the primary-selection | True | True | Single or Multiple
Selected | The ListViewItem is selected but not the primary | True | False | Multiple
Disabled | Not used | n/a | n/a | n/a

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

<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>

```
And set it like this:
```xml
`. You can bind to it or set your own collection, and if it supports `INotifyCollectionChanged` the `TreeViewZero` will track it.
SelectionMode | SelectionMode | Yes | Alloows a `SelectionMode` of None, Single or Multiple.
TreeChildren | IEnumerable | No | This is exposed for future capabilities and exposes all items *potentially* visible in the viewport.
TreeItemControlTemplate | ControlTemplate | Yes | Alows you to replace the default `ControlTemplate` used to render each node
TreeItemTemplate | TemplateProvider | Yes | Used 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:

Property | Type | Purpose
:----- | :---- | :-----
ChildrenPropertyName | string | The name of the property used to find the node children
IsExpandedPropertyName | string | The name of the property used to store whether the node is expanded
ItemTemplate | DataTemplate | The DataTemplate used to draw this node
TargetType | Type | When used in a `TreeItemDataTemplateSelector`, identifies the least-derived nodes the ItemTemplate can be applied to.

### Create a TreeViewZero

Given a hierarchy of `MyNode`
```csharp
public class MyNode
{
public string Name { get; set;}
public IEnumerable MyNodeChildren { get; set; }
}
```

Add the namespace:
```xml
xmlns:cz="clr-namespace:FunctionZero.Maui.Controls;assembly=FunctionZero.Maui.Controls"
```

Then declare a `TreeViewZero` like this:
```xml







```

## 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:
```csharp
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 Children {get; set;}
}
```
This is how to bind the `IsMyNodeExpanded` from our data, to `IsExpanded` on the TreeNode ...

```xml



...

```

## SelectionMode
The `TreeViewZero` allows selection modes *None, Single or Multiple*.
Please see the [ListViewZero](#selecteditem-selecteditems) 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](#styling-selecteditems) on the `ListViewZero` for details, or use the following as a guide:

```xml

<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>

```
And set it like this:
```xml





















```
### 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:

```csharp
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](https://github.com/Keflon/FunctionZero.Maui.Controls/blob/master/FunctionZero.Maui.Controls/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
1. Apply it to the `TreeViewZero`

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

Property | Type | Purpose
:----- | :----: | :-----
IsPrimary | bool | If selection is allowed, this tracks the current `SelectedItem`
IsSelected | bool | If the current item is selected, this is true. Note we can have multiple items *selected*, but only one *SelectedItem*
ItemIndex | int | For internal use when managing the cache
ItemTemplate | DataTemplate | The `DataTemplate` used to generate the `ListViewItem` Content

The `BindingContext` of the *templated parent* is a [TreeNodeContainer](https://github.com/Keflon/FunctionZero.TreeListItemsSourceZero) and includes the following properties:

Property | Type | Purpose
:----- | :----: | :-----
Indent | int | How deep the node should be indented. It is equal to `NestLevel`, or `NestLevel-1` if the Tree Root is not shown.
NestLevel | int | The depth of the node in the data.
IsExpanded | bool | This property reflects whether the TreeNode is expanded.
ShowChevron | bool | Whether the chevron is drawn. True if the node has children.
Data | object | This 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.
```xml






```

### Step 2 - give it to the TreeView ...
```xml







```

## 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` ...
```xaml







```
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`
```xaml




```

Finally, add to your `ViewModel` the properties the `ControlTemplate` binds to, and set them, simple as that!
```csharp
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:
- [CircleMaskPage.xaml](https://github.com/Keflon/FunctionZero.Maui.Controls/blob/master/SampleApp/Mvvm/Pages/Mask/CircleMaskPage.xaml)
- [CircleMaskPageVm.cs](https://github.com/Keflon/FunctionZero.Maui.Controls/blob/master/SampleApp/Mvvm/PageViewModels/Mask/CircleMaskPageVm.cs)

# Workarounds:

## `AdaptedTabbedPage` [MAUI bug 14572](https://github.com/dotnet/maui/issues/14572)
- Use it when you want to use `ItemsSource` and `ItemTemplate`. Stick with `TabbedPage` if you're manipulating the `Children` collection directly.
- This implementation replaces `ItemsSource` by hiding the base implementation.
This means if you set it up in code-behind, you must ensure you have a reference of type `AdaptedTabbedPage` when you set `ItemsSource`.
If your reference is of type `TabbedPage` or `MultiPage` you'll be setting the _base_ `ItemsSource` and the crash will remain.
### Update:
- ~~`SelectedItem` now has limited support. Setting it in code works fine and swaps to the correct Tab, however swapping by interacting with the UI does not
update `SelectedItem`, because doing so would cause the WinUI crash we're trying to dodge.~~
- `SelectedItem` is fine. If you think it's causing problems, set `UseExperimentalSelectedItem` to false.

## `AdaptedFlyoutPage` [MAUI bug 13496](https://github.com/dotnet/maui/issues/13496)
- Basically if the Flyout loses focus and the FlyoutLayoutBehavior is `Popover`,
it assumes the flyout has been dismissed and sets the `IsPresented` property to false.