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?
4 Answers, 1 is accepted
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
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"
?>
<
ContentPage
xmlns
=
"http://xamarin.com/schemas/2014/forms"
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
>
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
Hi Yana,
That worked - thanks for your help :)