Bind ListView to Nested Collection

5 posts, 1 answers
  1. Raymond
    Raymond avatar
    19 posts
    Member since:
    Jun 2019

    Posted 16 Jun 2019 Link to this post

    I am just starting with the Telerik Xamarin controls and the documentation uses a single observable collection to display and group data in the listview control.  My application uses an observable collection containing the data for a driver's itinerary for multiple days which in turn contain an observable collection of jobs for each day.

    Is it possible to bind the group headers to the "runs" collection and the item template to the "jobs" collection?

  2. Answer
    Yana
    Admin
    Yana avatar
    5031 posts

    Posted 17 Jun 2019 Link to this post

    Hello Raymond,

    Thank you for your interest in Telerik UI for Xamarin.
     
    It seems from the description that you have hierarchical data and the most suitable component for visualizing such type of data is RadTreeView. You could customize the look & feel of the TreeView items by creating a custom ItemTemplate as well as through defining ItemStyle/ ItemStyleSelector.

    I've attached a sample example following the described scenario to show you how binding would work, please download it and give it a try.

    RadListView, on the other hand, is used to visualize a list of items, so the grouping is executed per concrete criteria of these items.  With ListView the described scenario could be achieved with nested ListView control inside the item template of the root ListView.  Still, I would definitely recommend you try the approach with TreeView.

    Let me know if you have any additional questions on this.

    Regards,
    Yana
    Progress Telerik
    Do you want to have your say when we set our development plans? Do you want to know when a feature you care about is added or when a bug fixed? Explore the Telerik Feedback Portal and vote to affect the priority of the items
  3. Raymond
    Raymond avatar
    19 posts
    Member since:
    Jun 2019

    Posted 17 Jun 2019 in reply to Yana Link to this post

    Hi Yana,

    Thanks for that.  I refactored my code to use the TreeView control but unfortunately I ran into some issues with it.  I modified my page as follows;

    <?xml version="1.0" encoding="UTF-8"?>
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 xmlns:viewModels="clr-namespace:SrxRunSheet.ViewModels"
                 xmlns:telerikListView="clr-namespace:Telerik.XamarinForms.DataControls.ListView;assembly=Telerik.XamarinForms.DataControls"
                 xmlns:telerikDataControls="clr-namespace:Telerik.XamarinForms.DataControls;assembly=Telerik.XamarinForms.DataControls"
                 xmlns:telerikCommands="clr-namespace:Telerik.XamarinForms.DataControls.ListView.Commands;assembly=Telerik.XamarinForms.DataControls"
                 x:Class="SrxRunSheet.Views.RunSheet">
     
        <ContentPage.Content>
            <Grid
                RowSpacing="0"
                ColumnSpacing="0"
                BackgroundColor="White"><!--{StaticResource DarkSlateGrey}-->
     
                <Grid.RowDefinitions>
                    <RowDefinition Height="auto" />
                    <RowDefinition Height="*" />
                    <RowDefinition Height="50" />
                </Grid.RowDefinitions>
     
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="auto" />
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="auto" />
                </Grid.ColumnDefinitions>
     
                <!-- Page Content -->
                <telerikDataControls:RadTreeView x:Name="treeView" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3"  ItemsSource="{Binding RunList}">
                    <!--<telerikDataControls:TreeViewDescriptor DisplayMemberPath="RunStart" TargetType="{x:Type viewModels:DriverRunVm}" />-->
                    <telerikDataControls:TreeViewDescriptor DisplayMemberPath="RunStart" TargetType="{x:Type viewModels:DriverRunVm}" ItemsSourcePath="Jobs" />
                    <telerikDataControls:TreeViewDescriptor DisplayMemberPath="JobLine1" TargetType="{x:Type viewModels:JobVm}"/>
                </telerikDataControls:RadTreeView>
            </Grid>
        </ContentPage.Content>
    </ContentPage>

     

    The code to populate the models is;

    private async void DisplayRun(DateTime runDt)
    {
        try
        {
            // Get the list of run IDs applicable to the run date...
            List<string> runIds = new List<string>();
     
            foreach (var run in driverRuns)
            {
                if ((run.RunStart.ToLocalTime().Date == runDt.Date || run.RunFinish.ToLocalTime().Date == runDt)
                    || (run.RunStart.ToLocalTime().Date < runDt.Date && run.RunFinish.ToLocalTime().Date > runDt.Date))
                {
                    if (!runIds.Contains(run.Id)) runIds.Add(run.Id);
                }
            }
     
            if (runIds.Count() == 0)
                throw new Exception($"Failed to locate any run records that include any jobs for {runDt.ToString("ddd, d MMM yyyy")}");
     
            var runs = driverRuns.Where(x => runIds.Contains(x.Id)).OrderBy(x => x.RunStart);
            var jobResponseService = new JobResponseService(this.realm);
            var currentRunId = string.Empty;
     
            DriverRunVm driverRun = null;
            RunList = new ObservableCollection<DriverRunVm>();
     
            foreach (var run in runs.OrderBy(x => x.RunStart))
            {
                if (currentRunId != run.Id)
                {
                    if (currentRunId.HasValue()) RunList.Add(driverRun);
                    driverRun = new DriverRunVm(run);
                    currentRunId = run.Id;
                }
     
                int count = 0;
     
                foreach (var jobItem in run.Jobs.OrderBy(x => x.Sequence))
                {
                    var job = new JobVm(jobItem)
                    {
                        DriverId = driver.Id,
                        RunId = run.Id
                    };
     
                    // Get the last response record for this job...
                    var response = jobResponseService.GetLastByJobId(job.Id);
                    if (response != null && job.DriverId == response.DriverId && job.RunId == response.RunId)
                    {
                        job.Completed = response.Completed;
                        job.DriverNote = response.DriverNote;
                        job.Reason = response.Reason;
                        job.ReasonId = response.ReasonId;
                    }
     
                    driverRun.Jobs.Add(job);
     
                    count++;
                    if (count >= 1) break;
                }
            }
     
            RunList.Add(driverRun);
            RunDate = runDt;
        }
        catch (Exception ex)
        {
            await DisplayAlert("Display Run Sheet", ex.Message, "OK");
            return;
        }
    }

     

    And the classes are;

    namespace SrxRunSheet.ViewModels
        public class DriverRunVm : ObservableCollection<JobVm>, INotifyPropertyChanged
        {
            private readonly Run run;
     
            public DriverRunVm(Run run)
            {
                this.run = run;
                run.PropertyChanged += PropertyUpdated;
            }
     
            public ObservableCollection<JobVm> Jobs = new ObservableCollection<JobVm>();
            public string RunId { get { return run.Id; } }
            public DateTime RunStartDt { get { return run.RunStart.UtcDateTime.ToLocalTime(); } }
            public string RunStart { get { return FormatDate(RunStartDt); } }
            public DateTime RunFinishDt { get { return run.RunFinish.UtcDateTime.ToLocalTime(); } }
            public string RunFinish { get { return FormatDate(RunFinishDt); } }
            public string RunHeader { get { return $"{RunStart} - {RunFinish}"; } }
     
            private string FormatDate(DateTime value)
            {
                return $"{value.ToString("ddd, d MMM yyyy").Replace(".", "")} - {value.ToString("h:mmtt").ToLower()}";
            }
     
            private void PropertyUpdated(object sender, PropertyChangedEventArgs e)
            {
                OnPropertyChanged(e);
            }
    }

     

    namespace SrxRunSheet.ViewModels
    {
        public class JobVm : INotifyPropertyChanged
        {
            private bool isExpanded;
            private string runId;
            private string driverId;
            private readonly Job job;
            public event PropertyChangedEventHandler PropertyChanged;
     
            public JobVm(Job job)
            {
                this.job = job;
                this.job.PropertyChanged += PropertyUpdated;
            }
     
            public string CAN { get { return job.CAN; } }
            public string ClientName { get { return job.ClientName.Replace("  ", " "); } }
            public string ContactName { get { return job.ContactName; } }
            public string ContactPhone { get { return job.ContactPhone; } }
            public string ContactDetail { get { return SetContactDetail(); } }
            public string Description { get { return SetDescription(); } }
            public string FlatAddress { get { return job.JobAddress.Replace("|", " "); } }
            public string Id { get { return job.JobId; } }
            public string Instructions { get { return job.Instructions; } }
            public string JobAddress { get { return job.JobAddress.Replace("|", Environment.NewLine); } }
            public DateTime JobDate { get { return job.JobDate.UtcDateTime.ToLocalTime(); } }
            public string RequestedBy { get { return job.RequestedBy; } }
            public Run Run { get { return job.Run; } }
            public string Reason { get; set; }
            public int? ReasonId { get; set; }
            public int? Serviced { get; set; }
            public float? Weight { get; set; }
     
            public string JobLine1
            {
                get
                {
                    if (job.JobLine1.HasValue())
                        return job.JobLine1
                            .Replace("  ", " ")
                            .Strip(Environment.NewLine, StringExtensions.StripFrom.End);
     
                    return ClientName;
                }
            }
     
            public string DriverId
            {
                get { return driverId; }
     
                set
                {
                    if (driverId != value)
                    {
                        driverId = value;
                        PropertyUpdated(nameof(DriverId));
                    }
                }
            }
     
            public string JobIcon
            {
                get
                {
                    if (IsExpanded)
                        return FontAwesome.Solid.Chevron_Circle_Up;
                    else
                        return FontAwesome.Solid.Chevron_Circle_Down;
                }
            }
     
            public bool IsExpanded
            {
                get { return isExpanded; }
     
                set
                {
                    if (isExpanded != value)
                    {
                        isExpanded = value;
                        PropertyUpdated(nameof(IsExpanded));
                    }
                }
            }
     
            public string RunId
            {
                get { return runId; }
     
                set
                {
                    if (runId != value)
                    {
                        runId = value;
                        PropertyUpdated(nameof(RunId));
                    }
                }
            }
     
            public int Sequence
            {
                get { return job.Sequence; }
     
                set
                {
                    job.Sequence = value;
                    PropertyUpdated(nameof(Sequence));
                }
            }
     
            public string SequenceNumber
            {
                get { return Sequence.ToString().Trim(); }
            }
     
            private string SetContactDetail()
            {
                StringBuilder contact = new StringBuilder();
     
                if (job.ContactName.HasValue()) contact.AppendLine(job.ContactName);
                if (job.ContactPhone.HasValue()) contact.AppendLine(job.ContactPhone);
     
                if (contact.Length == 0) return "";
                return (contact.ToString().Strip(Environment.NewLine, StringExtensions.StripFrom.End));
            }
     
            private void PropertyUpdated(string propertyName)
            {
                OnPropertyChanged(propertyName);
                switch (propertyName)
                {
                    case nameof(ContactName):
                    case nameof(ContactPhone):
                        OnPropertyChanged(nameof(ContactDetail));
                        break;
                    case nameof(Completed):
                        OnPropertyChanged(nameof(JobStatus));
                        break;
                    case nameof(IsExpanded):
                        OnPropertyChanged(nameof(JobIcon));
                        break;
                }
            }
     
            private void PropertyUpdated(object sender, PropertyChangedEventArgs e)
            {
                PropertyUpdated(e.PropertyName);
            }
     
            private string SetDescription()
            {
                string description = "Error: No services have been included!";
     
                if (Services.Count == 1)
                {
                    var service = Services.First();
                    description = $"{service.Booked} x {service.Description}";
                }
                else if (Services.Count > 1)
                {
                    description = "Multiple services";
                }
     
                return description;
            }
     
            protected virtual void OnPropertyChanged(string propertyName)
            {
                Trace.WriteLine($"{DateTime.Now.ToString("hh:mm:ss.fff")} := Property updated - {propertyName}");
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }

     

    When I run the application crashes with the error "System.NullReferenceException: 'Object reference not set to an instance of an object.'"

    I i modify the page to use the following code the page displays a single record as expected;

    <telerikDataControls:RadTreeView x:Name="treeView" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3"  ItemsSource="{Binding RunList}">
        <telerikDataControls:TreeViewDescriptor DisplayMemberPath="RunStart" TargetType="{x:Type viewModels:DriverRunVm}" />
    </telerikDataControls:RadTreeView>

     

  4. Yana
    Admin
    Yana avatar
    5031 posts

    Posted 18 Jun 2019 Link to this post

    Hello Raymond,

    Thank you for sending the code.

    I've managed to reproduce it and the reason is that the Jobs collection inside the DriverRunVm class does not have a setter, hence ItemsSourcePath of the TreeViewDescriptor cannot be applied.  Please change it like this:

    public ObservableCollection<JobVm> Jobs { get; set; }
     
    public DriverRunVm(Run run)
    {
        this.Jobs = new ObservableCollection<JobVm>();
        this.run = run;
        run.PropertyChanged += PropertyUpdated;
    }

    Give this a try and let me know how it goes.

    Regards,
    Yana
    Progress Telerik
    Do you want to have your say when we set our development plans? Do you want to know when a feature you care about is added or when a bug fixed? Explore the Telerik Feedback Portal and vote to affect the priority of the items
  5. Raymond
    Raymond avatar
    19 posts
    Member since:
    Jun 2019

    Posted 18 Jun 2019 in reply to Yana Link to this post

    Hi Yana,

    That worked - thanks for your help :)

Back to Top