RadzenTree - Custom Checkbox behavior

Greetings, and thanks for your great components.

I understand that your RadzenTree component is made with the idea of encapsulating the complex selection-in-cascade logic and to give the user a finished and working component.

But there are some aspects of its selecting logic that doesn't work for me and I already have such a logic implemented inside my data class. So I'm asking to myself whether there is a way for handling myself this logic inside your RadzenTree component. What I'd need is a OnChecked(value, isSelected) event fired whenever an item is checked/unchecked. This way, my code could do all the logic for selecting in cascade.

Your solution of firing the "CheckedValuesChanged" event with a "CheckedValues" list, containing all selected items, may be useful in some cases but I think it lacks of some needed functionality:

Think of a file explorer tree, with many levels of sub-branches:

1.- Whenever an item is selected/deselected, in order to reflect those changes onto my hierarchical data list I must deselect all my elements first and select back all of them again if they are in the CheckedValues list that is provided with the event.

2.- This CheckedValues list presents the items in a flat way so that, if a parent is selected you will have this parent item and all its child items flattened in the list. Then, to correctly reflect this in my data tree I must do a lot of operations for propagating this flat list onto the hierarchy of mine, while separating parents from children.

3.- When the user selects a parent, its children doesn't appear in the "CheckedValues" list until the user expands its branch. So, if this branch has twenty sub-branches the user should open all of them for all those items to be included in the list. At a first glance, you could think that this point solves the problem in point 2 but, really, it adds to it.

(The list above is a result of my tries with your component. Please, excuse me if some of them are actually my own fault.)
So, my question is: is there some way to replace the checkboxes' behaviors with my own code?

Thank you.

You could probably use the Template option and render your own checkboxes that implement the required business logic.

Yes, I had already thought about it, but hoped there was another way that I was missing.

Thank you.

Following Atanas Korchev's advice, I've implemented my own checkboxes with custom behavior.
In case somebody else is searching for the same subject, I'm exposing below what I've done. I'm still working on it, because my selecting logic seems to have some flaws but, for the time being, this solution seems to work.

Previous considerations:
1.- Even though I'm an old I.T. guy, I'm new in C#; ASP.NET; BLAZOR and RADZEN components, so most likely there is a more correct way to do this.
2.- I have implemented a class "FETypes.ItemInfo" whose instances will populate the RadzenTree component. This class have a member "Children" which is a List of "FETypes.ItemInfo" elements.
3.- I have a hierarchical List of <FETypes.ItemInfo> elements. Each of these elements can have in its Children member another list of FETypes.ItemInfo elements, whose elements can also have children elements, etc. As you might already figured out, it is part of a Remote File Explorer component I'm developing.
4.- The class FETypes.ItemInfo has a property "bool IsSelected" and a method "OnItemSelected(bool)" which contains the logic for propagating down and populating up the item's checked status and statistics counters along its children and parents. The goal is to replace the default checkboxes behavior of RadzenTree with the logic implemented in my class.

In my component's .razor file:

@* We doesn't allow the default checkboxes because we are implementing custom ones.
*@
<RadzenTree class="folderTree" Data=@lstItemInfos Change=@OnValueChange AllowCheckBoxes=false>
	@*
	With the Expanded parameter's expression, we initially Expand only the root branch,
	because it is the only one which doesn't have a parent (I figured it out but maybe
	there is a better way). The important thing here is the Template parameter.
	*@
	<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) />
</RadzenTree>

@code {
	RenderFragment<RadzenTreeItem> ItemRenderTemplate = (context) => builder =>
	{
		FETypes.ItemInfo? pItem = context.Value as FETypes.ItemInfo;
		if(pItem is not null) {
			bool bIsDirectory = (pItem.ItemType == FETypes.ItemTypes.Directory);

			// Implement a checkbox with custom behavior.
			//
			builder.OpenComponent<RadzenCheckBox<bool?>>(0);
			builder.AddAttribute(1, nameof(RadzenCheckBox<bool?>.Value), pItem.IsSelected);
			builder.AddAttribute(2, nameof(RadzenCheckBox<bool?>.Change),
					new EventCallback<bool?>(null, pItem.OnItemSelected));
			builder.AddAttribute(3, "class", "treenode-checkbox");
			builder.CloseComponent();

			// Add an icon depending on the item's type.
			//
			builder.OpenComponent<RadzenIcon>(4);
			builder.AddAttribute(5, nameof(RadzenIcon.Icon), bIsDirectory ? "folder" : "insert_drive_file");
			builder.AddAttribute(6, "class", bIsDirectory ? "treenode-folder-icon" : "treenode-file-icon");
			builder.CloseComponent();
			builder.AddContent(7, context.Text);
		}
	};

}

Please, any ideas, criticisms and corrections are welcome.

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:

  1. 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.
  2. 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.

1 Like