Skip to content

Commit

Permalink
Merge pull request AvaloniaUI#86 from AvaloniaUI/feature/templates-fr…
Browse files Browse the repository at this point in the history
…om-resources

Allow defining TemplateColumn cells in XAML
  • Loading branch information
grokys authored Oct 19, 2022
2 parents ad8e175 + f2996d8 commit babd474
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 62 deletions.
40 changes: 35 additions & 5 deletions docs/column-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,44 @@ The sample above is taken from [this article](https://github.com/AvaloniaUI/Aval
## TemplateColumn
TemplateColumn is the most customizable option to create a column. You can put basically everything that you can put into `IDataTemplate` into this column cells.

That's how you instantiate `TemplateColumn` class:
There are two ways to instantiate a `TemplateColumn`:

1. Using a `FuncDataTemplate` to create a template in code:

```csharp
new TemplateColumn<Person>(
"Selected",
new FuncDataTemplate<FileTreeNodeModel>((a,e) => new CheckBox()))
"Selected",
new FuncDataTemplate<Person>((_, _) => new CheckBox
{
[!CheckBox.IsCheckedProperty] = new Binding("IsSelected"),
}))
```

2. Using a template defined as a XAML resource:

```xml
<TreeDataGrid Name="fileViewer" Source="{Binding Files.Source}">
<TreeDataGrid.Resources>

<!-- Defines a named template for the column -->
<DataTemplate x:Key="CheckBoxCell">
<CheckBox IsChecked="{Binding IsSelected}"/>
</DataTemplate>

</DataTemplate>

</TreeDataGrid.Resources>
</TreeDataGrid>
```

```csharp
// CheckBoxCell is the key of the template defined in XAML.
new TemplateColumn<Person>("Selected", "CheckBoxCell");
```

`TemplateColumn` has only one generic parameter, it is your model type, same as in `TextColumn`, Person in this case. Code above will create a column with header *"Selected"* and `CheckBox` in each cell.

![image](https://user-images.githubusercontent.com/53405089/157664231-8653bce9-f8d6-4fbc-8e78-e3ff93f1ace2.png)
`TemplateColumn` has two required parameters. The first one is the column header as everywhere. The second is either:

`TemplateColumn` has only two required parameters. The first one is the column header as everywhere. The second is `IDataTemplate` basically, a template that contains stuff that you want to be displayed in the cells of this column.
1. An `IDataTemplate`; a template that contains stuff that you want to be displayed in the cells of this column.
2. The key of a template defined in a XAML resource.
21 changes: 21 additions & 0 deletions samples/TreeDataGridDemo/MainWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:m="using:TreeDataGridDemo.Models"
xmlns:vm="using:TreeDataGridDemo.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="TreeDataGridDemo.MainWindow"
Title="TreeDataGridDemo">
Expand Down Expand Up @@ -53,6 +55,25 @@
</DockPanel>
<TextBlock Classes="realized-count" DockPanel.Dock="Bottom"/>
<TreeDataGrid Name="fileViewer" Source="{Binding Files.Source}">
<TreeDataGrid.Resources>

<!-- Template for Name column cells -->
<DataTemplate x:Key="FileNameCell" DataType="m:FileTreeNodeModel">
<StackPanel Orientation="Horizontal">
<Image Margin="0 0 4 0"
VerticalAlignment="Center">
<Image.Source>
<MultiBinding Converter="{x:Static vm:FilesPageViewModel.FileIconConverter}">
<Binding Path="IsDirectory"/>
<Binding Path="IsExpanded"/>
</MultiBinding>
</Image.Source>
</Image>
<TextBlock Text="{Binding Name}" VerticalAlignment="Center"/>
</StackPanel>
</DataTemplate>

</TreeDataGrid.Resources>
<TreeDataGrid.Styles>
<Style Selector="TreeDataGrid TreeDataGridRow:nth-child(2n)">
<Setter Property="Background" Value="#fff8f8f8"/>
Expand Down
3 changes: 2 additions & 1 deletion samples/TreeDataGridDemo/Models/FileTreeNodeModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace TreeDataGridDemo.Models
{
internal class FileTreeNodeModel : ReactiveObject
public class FileTreeNodeModel : ReactiveObject
{
private string _path;
private string _name;
Expand All @@ -27,6 +27,7 @@ public FileTreeNodeModel(
_name = isRoot ? path : System.IO.Path.GetFileName(Path);
_isExpanded = isRoot;
IsDirectory = isDirectory;
HasChildren = isDirectory;

if (!isDirectory)
{
Expand Down
68 changes: 21 additions & 47 deletions samples/TreeDataGridDemo/ViewModels/FilesPageViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,23 @@
using Avalonia.Controls;
using Avalonia.Controls.Models.TreeDataGrid;
using Avalonia.Controls.Selection;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Layout;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using ReactiveUI;
using TreeDataGridDemo.Models;

namespace TreeDataGridDemo.ViewModels
{
internal class FilesPageViewModel : ReactiveObject
public class FilesPageViewModel : ReactiveObject
{
private static IconConverter? s_iconConverter;
private FileTreeNodeModel? _root;
private string _selectedDrive;
private string? _selectedPath;
private FolderIconConverter? _folderIconConverter;

public FilesPageViewModel()
{
var assetLoader = AvaloniaLocator.Current.GetService<IAssetLoader>();

if (assetLoader is not null)
{
using (var fileStream = assetLoader.Open(new Uri("avares://TreeDataGridDemo/Assets/file.png")))
using (var folderStream = assetLoader.Open(new Uri("avares://TreeDataGridDemo/Assets/folder.png")))
using (var folderOpenStream =
assetLoader.Open(new Uri("avares://TreeDataGridDemo/Assets/folder-open.png")))
{
var fileIcon = new Bitmap(fileStream);
var folderIcon = new Bitmap(folderStream);
var folderOpenIcon = new Bitmap(folderOpenStream);

_folderIconConverter = new FolderIconConverter(fileIcon, folderOpenIcon, folderIcon);
}
}
Drives = DriveInfo.GetDrives().Select(x => x.Name).ToList();

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
Expand All @@ -71,7 +52,7 @@ public FilesPageViewModel()
new HierarchicalExpanderColumn<FileTreeNodeModel>(
new TemplateColumn<FileTreeNodeModel>(
"Name",
new FuncDataTemplate<FileTreeNodeModel>(FileNameTemplate, true),
"FileNameCell",
new GridLength(1, GridUnitType.Star),
new ColumnOptions<FileTreeNodeModel>
{
Expand Down Expand Up @@ -131,35 +112,28 @@ public string? SelectedPath

public HierarchicalTreeDataGridSource<FileTreeNodeModel> Source { get; }

private IControl FileNameTemplate(FileTreeNodeModel node, INameScope ns)
public static IMultiValueConverter FileIconConverter
{
return new StackPanel
get
{
Orientation = Orientation.Horizontal,
VerticalAlignment = VerticalAlignment.Center,
Children =
if (s_iconConverter is null)
{
new Image
{
[!Image.SourceProperty] = new MultiBinding
{
Bindings =
{
new Binding(nameof(node.IsDirectory)),
new Binding(nameof(node.IsExpanded)),
},
Converter = _folderIconConverter,
},
Margin = new Thickness(0, 0, 4, 0),
VerticalAlignment = VerticalAlignment.Center,
},
new TextBlock
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();

using (var fileStream = assetLoader.Open(new Uri("avares://TreeDataGridDemo/Assets/file.png")))
using (var folderStream = assetLoader.Open(new Uri("avares://TreeDataGridDemo/Assets/folder.png")))
using (var folderOpenStream = assetLoader.Open(new Uri("avares://TreeDataGridDemo/Assets/folder-open.png")))
{
[!TextBlock.TextProperty] = new Binding(nameof(FileTreeNodeModel.Name)),
VerticalAlignment = VerticalAlignment.Center,
var fileIcon = new Bitmap(fileStream);
var folderIcon = new Bitmap(folderStream);
var folderOpenIcon = new Bitmap(folderOpenStream);

s_iconConverter = new IconConverter(fileIcon, folderOpenIcon, folderIcon);
}
}
};

return s_iconConverter;
}
}

private void SetSelectedPath(string? value)
Expand Down Expand Up @@ -229,13 +203,13 @@ private void SelectionChanged(object? sender, TreeSelectionModelSelectionChanged
System.Diagnostics.Trace.WriteLine($"Selected '{i?.Path}'");
}

private class FolderIconConverter : IMultiValueConverter
private class IconConverter : IMultiValueConverter
{
private readonly Bitmap _file;
private readonly Bitmap _folderExpanded;
private readonly Bitmap _folderCollapsed;

public FolderIconConverter(Bitmap file, Bitmap folderExpanded, Bitmap folderCollapsed)
public IconConverter(Bitmap file, Bitmap folderExpanded, Bitmap folderCollapsed)
{
_file = file;
_folderExpanded = folderExpanded;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@

using System;
using Avalonia.Controls.Templates;

namespace Avalonia.Controls.Models.TreeDataGrid
{
public class TemplateCell : ICell
{
public TemplateCell(object? value, IDataTemplate? cellTemplate)
public TemplateCell(object? value, Func<IControl, IDataTemplate> getCellTemplate)
{
CellTemplate = cellTemplate;
GetCellTemplate = getCellTemplate;
Value = value;
}

public bool CanEdit => false;
public IDataTemplate? CellTemplate { get; }
public Func<IControl, IDataTemplate> GetCellTemplate { get; }
public object? Value { get; }
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Avalonia.Controls.Templates;

Expand All @@ -14,6 +15,9 @@ public class TemplateColumn<TModel> : ColumnBase<TModel>, ITextSearchableColumn<
{
private readonly Comparison<TModel?>? _sortAscending;
private readonly Comparison<TModel?>? _sortDescending;
private readonly Func<IControl, IDataTemplate> _getCellTemplate;
private IDataTemplate? _cellTemplate;
private object? _cellTemplateResourceKey;

public TemplateColumn(
object? header,
Expand All @@ -24,22 +28,52 @@ public TemplateColumn(
{
_sortAscending = options?.CompareAscending;
_sortDescending = options?.CompareDescending;
CellTemplate = cellTemplate;
_getCellTemplate = GetCellTemplate;
_cellTemplate = cellTemplate;
}

public TemplateColumn(
object? header,
object cellTemplateResourceKey,
GridLength? width = null,
ColumnOptions<TModel>? options = null)
: base(header, width, options)
{
_sortAscending = options?.CompareAscending;
_sortDescending = options?.CompareDescending;
_getCellTemplate = GetCellTemplate;
_cellTemplateResourceKey = cellTemplateResourceKey ??
throw new ArgumentNullException(nameof(cellTemplateResourceKey));
}

public Func<TModel, string?>? TextSearchValueSelector { get; set; }
public bool IsTextSearchEnabled { get; set; }


/// <summary>
/// Gets the template to use to display the contents of a cell that is not in editing mode.
/// </summary>
public IDataTemplate? CellTemplate { get; }
public Func<TModel, string?>? TextSearchValueSelector { get; set; }
public bool IsTextSearchEnabled { get; set; }
public IDataTemplate GetCellTemplate(IControl anchor)
{
if (_cellTemplate is not null)
return _cellTemplate;

_cellTemplate = anchor.FindResource(_cellTemplateResourceKey!) as IDataTemplate;

if (_cellTemplate is null)
throw new KeyNotFoundException(
$"No data template resource with the key of '{_cellTemplateResourceKey}' " +
"could be found for the template column '{Header}'.");

return _cellTemplate;
}

/// <summary>
/// Creates a cell for this column on the specified row.
/// </summary>
/// <param name="row">The row.</param>
/// <returns>The cell.</returns>
public override ICell CreateCell(IRow<TModel> row) => new TemplateCell(row.Model, CellTemplate);
public override ICell CreateCell(IRow<TModel> row) => new TemplateCell(row.Model, _getCellTemplate);

public override Comparison<TModel?>? GetComparison(ListSortDirection direction)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using Avalonia.Controls.Models.TreeDataGrid;
using Avalonia.Controls.Templates;
using Avalonia.LogicalTree;

namespace Avalonia.Controls.Primitives
{
Expand Down Expand Up @@ -42,6 +43,14 @@ public override void Unrealize()
base.Unrealize();
}

protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);

if (ContentTemplate is null && DataContext is TemplateCell cell)
ContentTemplate = cell.GetCellTemplate(this);
}

protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
Expand All @@ -53,7 +62,9 @@ protected override void OnDataContextChanged(EventArgs e)
if (cell is not null)
{
Content = cell.Value;
ContentTemplate = cell.CellTemplate;

if (((ILogical)this).IsAttachedToLogicalTree)
ContentTemplate = cell.GetCellTemplate(this);
}
else
{
Expand Down

0 comments on commit babd474

Please sign in to comment.