In order to close this subject ("RadzenTree - Custom Checkbox behavior") with a complete solution, I'm writing this final lines.
As I see it, the RadzenTree control is a good closed control for those who needs a standard one, but I miss some open doors for those who wants to customize things a little.
For example, let's think of two classic uses for a tree control:
a. A file Explorer.
b. An Installer for a software with many optional components.
In those examples, the developer will encounter at least with two impediments:
- You might want to supply your own selecting logic or to react somehow after a checkbox is changed. This control fires the "Change" event when the user clicks on a tree's item but doesn't fires one when the user clicks in a checkbox. Of course it fires an event whenever any checkbox changes its value but you won't know which checkbox was the changed one.
- Whenever an user clicks in a checkbox, the tree's item in which the checkbox resides gains focus and gets selected. But think of a Folder tree in a file explorer. You have the focus in a parent directory while selecting other subdirectories or files for doing something with them. As long as the current selected directory still the same, some info or statistic counters about this folder are being displayed, and you are monitoring them at the same time that you are selecting their children.
Also think about a software installer. Usually you activate (click on) a software group in order to see in another panel the information about this software and the amount of space on disk it will use while you are selecting (clicking on the checkboxes) of its software components. You don't want the focus to be changed and this information to disappear every time you check a component in order to include or exclude it from the installation.
Regarding point #2, I think the selecting process (making a tree's item active) should be fully differentiated from the checking process, but they are tied because the checkbox is implemented as a sub-part of the tree item's text.
Fortunately, there is at least a way I have found to achieve all above and I'm explaining it below, in case any other user is trying to do the same.
I'm developing a remote file explorer in Blazor. This component is divided into 4 parts:
⦁ The main File Explorer or the "view", parent of all of them: FileExplorer_Radzen.razor
⦁ A folder tree component: FolderTree_Radzen.razor
⦁ A file list component: FileList_Radzen.razor
⦁ A panel with statistics: (defined in the same file as the view).
I want the main component, FileExplorer_Radzen, to coordinate and drive the behavior of every other sub-component. For that, this component needs to receive some events from its children components. We will create the checkboxes during the rendering of every TreeItem and we want this code to send an event to the topmost component of the chain and to tell which data object must be selected/deselected . The major problem is that our code in the RenderFragment function will have a local scope and the RadzenCheckbox component fires an event whose handler must be a static function with a single boolean argument. I've solved it as follows.
The main component, the File Explorer:
The first thing I've done is, in a shared .cs file, to derive a new class from RadzenTree, called "ToRadzenTree", with a new member "Parent". This member will contain (a pointer to) the parent component of the FolderTree (the File Explorer).
I have also defined a new event "SelectionEventArgs" which will be fired by the tree's checkboxes.
namespace To.RemoteFileSystem.Client.Components.FileExplorer_Radzen
{
public class ToRadzenTree : RadzenTree
{
[Parameter, EditorRequired]
public FolderTree_Radzen? Parent { get; set; } = null;
internal void SetParent(FolderTree_Radzen pParent) { Parent = pParent; }
}
public class SelectionEventArgs : EventArgs
{
public FETypes.ItemInfo? pItem;
public bool? bSelect;
}
}
This event will have two arguments:
⦁ the data element (an object's pointer) whose checkbox has changed.
⦁ the new state of the checkbox.
In FileExplorer_Radzen.razor
, I've instantiated the folders tree component this way:
<RadzenCard class="rz-shadow-6 folderTree-card">
<FolderTree_Radzen @ref=_FolderTree LstItemInfos="@lstItemInfos"
OnValueChange="@OnFolderTree_ValChgAsync"
OnCheckedChange="@OnFolderTree_CheckChgAsync"
/>
</RadzenCard>
LstItemInfos is a hierarchical List of data elements (List<ItemInfo>), which will be shown by the tree. The function for OnValueChange is the event handler that the original RadzenTree calls when the user clicks on an TreeItem element. The important thing here is our new event "OnCheckedChange", which will be fired every time the user clicks on a checkbox. Both the folders viewer (card) and its event handlers, are inside the File Explorer component. Here is the event handler for OncheckedChange.
public async Task OnFolderTree_CheckChgAsync(SelectionEventArgs eventArgs) {
FETypes.ItemInfo? pItem = eventArgs.pItem;
if (pItem is not null) {
await Task.Run(() => {
pItem.Select(
eventArgs.bSelect ?? false,
stcOptions.bSelectRecursive,
stcOptions.eSelectItemTypes
);
_displayStatistics();
});
}
}
In the above function, the variable "pItem" receives the element of the data list whose checkbox has been checked/unchecked. That element is a (pointer to an) instance of a "ItemInfo" class. Every instance of this class represents a folder or a file in the remote filesystem. This class also has a function member "Select()" that contains all my internal logic for selection, and replaces the RadzenTree internal behavior. Because the event handler is in the topmost component, every time a checkbox is changed in the tree we can manipulate all the other components of the file explorer.
The folders Tree sub-component:
In FolderTree_Radzen.razor
, I instantiated my derived tree component as follow:
<ToRadzenTree @ref=@TreeControl Parent=@this AllowCheckBoxes=false
Data=@LstItemInfos Change=@OnValueChange class="folderTree">
<RadzenTreeLevel TextProperty="ItemName" ChildrenProperty="Children" Template=@_itemRenderTemplate
HasChildren=@(childrenData =>
(childrenData as FETypes.ItemInfo)?.Children is not null)
Expanded=@(childrenData =>
(childrenData as FETypes.ItemInfo)?.Parent is null) />
</ToRadzenTree>
The important things above are:
⦁ I'm setting "this" component as the tree's Parent.
⦁ AllowCheckBoxes is false because I will implement the checkboxes in a template.
⦁ I'm declaring _itemRenderTemplate as a RenderFragment template function for the tree's items.
In the code section or .cs file, we declare the EventCallback delegate as a parameter:
[Parameter]
public EventCallback<SelectionEventArgs> OnCheckedChange { get; set; }
protected ToRadzenTree? TreeControl { get; set; } = null;
// We could also fix the misuse of ToRadzenTree.Parent
// at run-time this way
//
//protected override async Task OnAfterRenderAsync(bool firstRender) {
// await base.OnAfterRenderAsync(firstRender);
// if (firstRender && TreeControl is not null) {
// if (TreeControl.Parent != this)
// TreeControl.SetParent(this);
// }
//}
The RenderFragment TEMPLATE is the same as in my previous post but with these changes and additions:
// TriState=true will break our code.
// Fortunately, even when TriState=false the checkbox will switch to
// the semi-checked state when its Value=null.
//
builder.AddAttribute(1, nameof(RadzenCheckBox<bool?>.TriState), false);
// Allows to Fire an static external Event, handled by this component
// (the tree), which in turn will fire an event in the correct instance
// of our parent component when the user clicks in a checkbox.
//
builder.AddAttribute(3, nameof(RadzenCheckBox<bool?>.Change),
new EventCallback<bool?>(null, (bool? bValue) => {
_onItemCheckChangeAsync((ToRadzenTree)context.Tree, pItem, bValue);
})
);
// Don't change the active TreeItem (don't fire the tree's
// Change EventCallback) whenever the user clicks on the checkbox.
// I discovered how to do it by watching the generated (decompiled)
// code of the razor template in RadzenTreeItem.razor
//
builder.AddEventStopPropagationAttribute(4, "onclick", value: true);
Then, outside the RenderFragment TEMPLATE, we define our static local handler which will populate the event to the tree's parent (the File Explorer):
// Static event handler, which in turn fires an event in the correct instance
// of our parent component when the user clicks in a checkbox, passing as
// parameters the list's ItemInfo corresponding to the TreeItem whose checkbox
// has been clicked and the new value of the checkbox.
//
static private async void _onItemCheckChangeAsync(
ToRadzenTree oTree, FETypes.ItemInfo oItem, bool? bCheck ) {
FolderTree_Radzen? oParent; // The parent Component of this tree.
SelectionEventArgs eventArgs;
if (bCheck is not null) {
oParent = oTree.Parent;
if (oParent is not null) {
eventArgs = new() {
pItem = oItem,
bSelect = bCheck
};
await oParent.OnCheckedChange.InvokeAsync(eventArgs);
}
}
}
Now, it works as I needed.