New to Telerik UI for Xamarin? Download free 30-day trial

TreeView Disable horizontal scroll caused by long item names

Environment

Product Version 2021.1.119.1
Product TreeView for Xamarin

Description

For this purpose, we will need to implement a custom renderer for android and for iOS.

Solution

Here is a sample TreeView definition:

<ContentPage.Resources>
        <ResourceDictionary>
            <telerikTreeView:LevelToMarginConverter x:Key="levelToMarginConverter" />

            <DataTemplate x:Key="CustomControlTemplate">
                <StackLayout Orientation="Horizontal"
                     Margin="{Binding Path=Level, Converter={StaticResource levelToMarginConverter}}">
                    <StackLayout.HeightRequest>
                        <OnPlatform x:TypeArguments="x:Double">
                            <On Platform="iOS" Value="44"></On>
                            <On Platform="Android" Value="40"></On>
                            <On Platform="UWP" Value="40"></On>
                        </OnPlatform>
                    </StackLayout.HeightRequest>
                    <telerikTreeView:ExpandCollapseIndicator FontSize="Medium"
                                           WidthRequest="10"
                                           Margin="15,0"
                                           VerticalTextAlignment="Center"
                                           IsLoading="{Binding Path=IsLoading}"
                                           IsLoadOnDemandEnabled="{Binding Path=IsLoadOnDemandEnabled}"
                                           IsExpanded="{Binding Path=IsExpanded}"
                                           IsLeaf="{Binding Path=IsLeaf}"/>
                    <telerikPrimitives:RadCheckBox IsChecked="{Binding Path=IsChecked, Mode=TwoWay}"
                                    IsVisible="{Binding Path=IsCheckBoxVisible}"
                                    VerticalOptions="Center"/>
                    <telerikTreeView:ItemText Text="{Binding Path=Header}"
                                              VerticalTextAlignment="Center"
                                              MaxLines="1"
                                              LineBreakMode="TailTruncation"/>
                </StackLayout>
            </DataTemplate>
        </ResourceDictionary>
    </ContentPage.Resources>

    <Grid>
        <telerikDataControls:RadTreeView x:Name="treeView" ItemsSource="{Binding Source}">
            <telerikDataControls:TreeViewDescriptor DisplayMemberPath="Name"
                                            ItemsSourcePath="Children"
                                                    ItemTemplate="{StaticResource CustomControlTemplate}"
                                            TargetType="{x:Type local:Item}" />
        </telerikDataControls:RadTreeView>
    </Grid>

and the used ViewModel

public class Item
{
    public Item(string name)
    {
        this.Name = name;
        this.Children = new ObservableCollection<Item>();
    }

    public string Name { get; set; }
    public IList<Item> Children { get; set; }
}

public class ViewModel
{
    public ViewModel()
    {
        Source = new ObservableCollection<Item>();
        Source.Add(new Item("My Documents")
        {
            Children = new List<Item>()
        {
            new Item("Xamarin Projects")
            {
                Children = new ObservableCollection<Item>()
                {
                    new Item("TreeView Examples"),
                    new Item("Calendar & Scheduling QSF")
                }
            },
            new Item("Documentation Drafts")
        }
        });
        Source.Add(new Item("Shared Documents")
        {
            Children = new List<Item>()
        {
            new Item("Reports")
            {
                Children = new List<Item>()
                {
                    new Item("Long text text text text text text text text text text text text text text end"),
                    new Item("November"),
                    new Item("December")
                }
            }
        }
        });
    }
    public ObservableCollection<Item> Source { get; set; }
}

then the BindingContext set

this.BindingContext = new ViewModel();

Custom Renderer for Android

Custom Renderer implementation on Android

using System;
using System.Reflection;
using Android.Content;
using AndroidX.RecyclerView.Widget;
using RadTreeViewXF.Droid;
using Telerik.XamarinForms.DataControls;
using Telerik.XamarinForms.DataControls.TreeView;
using Telerik.XamarinForms.DataControlsRenderer.Android;
using Xamarin.Forms;
using Xamarin.Forms.Internals;
using Xamarin.Forms.Platform.Android;
using AndroidViews = Android.Views;

[assembly: ExportRenderer(typeof(RadTreeView), typeof(CustomTreeViewRenderer))]
namespace RadTreeViewXF.Droid
{
    public class CustomTreeViewRenderer : TreeViewRenderer
    {
        public CustomTreeViewRenderer(Context context)
            : base(context)
        {
        }

        protected override void OnElementChanged(ElementChangedEventArgs<RadTreeView> e)
        {
            base.OnElementChanged(e);

            var nativeTreeView = this.Control;
            if (nativeTreeView != null)
            {
                Registrar.Registered.Register(typeof(TreeViewTemplateCell), typeof(CustomTreeViewTemplateCellRenderer));
                nativeTreeView.SetLayoutManager(new LinearLayoutManager(this.Context));
            }
        }

        class CustomTreeViewTemplateCellRenderer : CellRenderer
        {
            protected override AndroidViews.View GetCellCore(Cell item, AndroidViews.View convertView, AndroidViews.ViewGroup parent, Context context)
            {
                var viewCell = (ViewCell)item;

                IVisualElementRenderer renderer = Platform.GetRenderer(viewCell.View);
                if (renderer == null || renderer.Element == null)
                {
                    renderer = Platform.CreateRendererWithContext(viewCell.View, context);
                    Platform.SetRenderer(viewCell.View, renderer);
                }

                return new TreeViewTemplateCellContainer(context, renderer, viewCell);
            }
        }

        class TreeViewTemplateCellContainer : FormsViewGroup
        {
            private static Type invalidationEventArgsType;
            private static PropertyInfo triggerProperty;

            protected readonly ViewCell viewCell;
            protected readonly IVisualElementRenderer view;
            private MethodInfo invalidateMeasureMethod;

            public TreeViewTemplateCellContainer(Context context, IVisualElementRenderer view, ViewCell viewCell)
                : base(context)
            {
                this.viewCell = viewCell;
                this.view = view;
                this.AddView(view.View);

                this.viewCell.View.MeasureInvalidated += this.OnMeasureInvalidated;
                this.invalidateMeasureMethod = this.view.Element.GetType().GetMethod("InvalidateMeasure", BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, new System.Type[] { }, new ParameterModifier[] { });
            }

            protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec)
            {
                var treeView = (RadExtendedListView)this.Parent;
                var width = treeView.Width;
                this.invalidateMeasureMethod.Invoke(this.view.Element, new object[] { });

                var sizeRequest = this.view.Element.Measure(double.PositiveInfinity, double.PositiveInfinity, MeasureFlags.IncludeMargins);

                var height = (int)this.Context.ToPixels(this.viewCell.Height > 0 ? this.viewCell.Height : sizeRequest.Request.Height);
                this.SetMeasuredDimension(width, height);
            }

            protected override void OnLayout(bool changed, int left, int top, int right, int bottom)
            {
                var context = base.Context;
                var width = context.FromPixels(right - left);
                var height = context.FromPixels(bottom - top);

                Xamarin.Forms.Layout.LayoutChildIntoBoundingRegion(this.view.Element, new Rectangle(0, 0, width, height));
                this.view.UpdateLayout();
            }

            private void OnMeasureInvalidated(object sender, System.EventArgs e)
            {
                var trigger = TryGetInvalidationTrigger(e);
                if (trigger.HasValue && trigger.Value.HasFlag(InvalidationTrigger.SizeRequestChanged) ||
                    trigger.Value.HasFlag(InvalidationTrigger.MarginChanged) ||
                    trigger.Value.HasFlag(InvalidationTrigger.MeasureChanged))
                {
                    this.RequestLayout();
                }
            }

            protected override void Dispose(bool disposing)
            {
                if (disposing)
                {
                    if (this.viewCell != null && this.viewCell.View != null)
                    {
                        this.viewCell.View.MeasureInvalidated -= this.OnMeasureInvalidated;
                        this.viewCell.View = null;
                    }
                }

                base.Dispose(disposing);
            }

            internal static InvalidationTrigger? TryGetInvalidationTrigger(EventArgs e)
            {
                Type type = e.GetType();
                if (invalidationEventArgsType == null)
                {
                    if (type.FullName == "Xamarin.Forms.InvalidationEventArgs")
                    {
                        invalidationEventArgsType = type;
                        triggerProperty = type.GetRuntimeProperty("Trigger");
                    }
                }

                if (type != invalidationEventArgsType)
                {
                    return null;
                }

                object propertyValue = triggerProperty.GetValue(e);
                InvalidationTrigger actualTrigger = (InvalidationTrigger)propertyValue;
                return actualTrigger;
            }
        }
    }
}

Custom Renderer for iOS

Custom Renderer implementation on iOS

using CoreGraphics;
using Foundation;
using RadTreeViewXF.iOS;
using Telerik.XamarinForms.DataControls;
using Telerik.XamarinForms.DataControlsRenderer.iOS;
using TelerikUI;
using Xamarin.Forms;

[assembly: ExportRenderer(typeof(RadTreeView), typeof(CustomTreeViewRenderer))]
namespace RadTreeViewXF.iOS
{
    public class CustomTreeViewRenderer : TreeViewRenderer
    {
        protected override TKTreeViewListView CreateNativeControl()
        {
            return new CustomTKTreeViewListView();
        }

        class CustomTKTreeViewListView : TKTreeViewListView
        {
            public override TKListViewLinearLayout CreateLayout()
            {
                return new CustomTreeViewLayout(this);
            }
        }
    }

    public class CustomTreeViewLayout : TreeViewLayout
    {
        TKListView listView;
        public CustomTreeViewLayout(TKListView listView)
            : base(listView)
        {
            this.listView = listView;
        }

        public override CGSize GetItemSizeForIndexPath(NSIndexPath indexPath)
        {
            return this.Delegate.SizeForItem(this.listView, this, indexPath);
        }
    }
}

In this article