Introduction
In this article, I’ll try to show how to beat the original Timeline view behavior (with appointments in viewport and “Show More” button) and make it scroll them.
Background
In my current project on Silverlight 3, I use Telerik’s control library that is really powerful and fancy. It provides you with a large number of different controls which supports themes, templates, etc. But as it often happens, you need something which goes against the idea of the library’s architects. In my case, it was an ability to show all appointments in timeline view and scroll them instead of default behavior which displays appointments in viewport only and provides you with “Show More” button.
I searched a lot for a solution, but even Telerik's support said that it is impossible in the current version and probably will be implemented in future.
The resulting solution looks like a combination of hacks that affect the harmony of code and performance of application, but it works and probably will help somebody to solve the same problem.
Steps of Solution
Originally Timeline view with a lot of appointments that do not fit to the viewport looks like this:
My goal was to receive the next view:
To achieve this, we should do the next steps:
- Remove “Show More” button
- Increase the height of appointments
- Enable vertical scroll
- Change scrolled control height according to the number of appointments
To resolve the first three problems without hacks in code, I decided to use custom theme, because theme was fixed in the project (Windows 7
) and probably it is the only way to do it. The penalty of this solution is increased size of assembly (because of huge style) and inability to change themes dynamically for this control.
I made all changes in the original Window 7 theme which is distributing with the “RadControls for Silverlight
” toolkit. Besides the above-listed steps, I added support for mouse wheel scrolling in ScrollViewer
and removed default behavior which changes a week back and forward instead. All changes in theme are marked with CHANGE notice in comments.
Here is the result of the mentioned changes:
But the problem still exists. If you do not have enough space to present your appointments, they will be cut by viewport. The reason is that they are located in the AppointmentItemsControl
which used VirtualizedAppointmentPanel
(derived from VirtualizingPanel
) that is always virtualized. It gets own height from TimeSlotItemsControl
which is just stretched within the available space.
Thus we should change TimeSlotItemsControl
’s height to archive the desired behavior. This height shall depend on maximum number of appointments per day for visible time range.
This behavior can be implemented by binding with special converter who will do the mentioned conversion from appointments source to desired height.
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
const int appointmentHeight = 45;
var presenter = (ScrollContentPresenter)parameter;
var scheduler = presenter.GetVisualParent<RadScheduler>();
var appointments = ((IEnumerable)value).Cast<Appointment>().
Where(a => a.Start >= scheduler.VisibleRangeStart &&
a.Start <= scheduler.VisibleRangeEnd);
var maxAppointmentsPerStack = (appointments.Count() == 0) ? 0 :
appointments.GroupBy(a => a.Start.Date).Max(g => g.Count());
var possibleHeight = (maxAppointmentsPerStack + 1) * appointmentHeight;
var presenterHeight = presenter.ViewportHeight;
return (possibleHeight > presenterHeight) ? possibleHeight : double.NaN;
}
To get an access to TimeSlotItemsControl
and ScrollContentPresenter
, I managed to use DataTemplate
for time slots and a simple TemplateSelector
to wire it to the scheduler.
private void onTimeSlotLoaded(object sender, RoutedEventArgs e)
{
var scrollContentPresenter =
((FrameworkElement)sender).GetVisualParent<ScrollContentPresenter>();
var timeSlotItemsControl =
scrollContentPresenter.FindChildByType<TimeSlotItemsControl>();
if (timeSlotItemsControl.GetBindingExpression(HeightProperty) == null)
{
timeSlotItemsControl.SetBinding(HeightProperty,
new Binding
{
Source = _scheduler,
Path = new PropertyPath("AppointmentsSource"),
Converter = HeightConverter.Instance,
ConverterParameter = scrollContentPresenter
});
}
}
XAML
<UserControl.Resources>
<DataTemplate x:Key="TimeLineSlotTemplate">
<Grid Loaded="onTimeSlotLoaded"/>
</DataTemplate>
<local:TimeSlotTemplateSelector x:Key="TimeSlotTemplateSelector"
TimeLineSlotTemplate="{StaticResource TimeLineSlotTemplate}"/>
<theme:SchedulerTheme x:Key="CustomTheme" />
</UserControl.Resources>
<Controls:RadScheduler
x:Name="_scheduler"
ViewMode="Timeline"
AvailableViewModes="Timeline"
TimeSlotTemplateSelector="{StaticResource TimeSlotTemplateSelector}"
telerik:StyleManager.Theme="{StaticResource CustomTheme}"/>
On this stage, I thought that work is done because I saw the scrollbar when it should be and didn’t see when it was unnecessary when I changed the current week. But the reality was too difficult to stop work, because view did not want to draw appointments under the viewport.
I tried to beat this behavior several times, I used Reflector, Silverlight Spy, tried to debug the library’s code, but couldn’t fix this behavior in an appropriate way. But I found the hack which affects performance but solves the problem – collection recreation on ScrollContentPresenter
resize. This approach caused the separation between original data source and view via some intermediate collection. We should listen to the original collection changes now and reflect them to intermediate collection.
Also intermediate collection should be recreated when you change visible range in scheduler to force height recalculation because new time range can have different maximum stack height.
I used collection that is derived from the BaseAppointmentCollection
from Telerik’s suite to solve another problem with scheduler behavior, but you could use your own.
private void onScrollContentPresenterSizeChanged(object sender, SizeChangedEventArgs e)
{
updateAppointmentsCollection();
}
private void updateAppointmentsCollection()
{
var collection = new AppointmentCollection();
collection.AddRange(_dataSource);
_scheduler.AppointmentsSource = collection;
}
The current solution works even for changing set of appointments, i.e., when you add new appointments, delete existing or just enlarge you data source with existing appointments arrived from web service during application work. This is possible because of intermediate collection recreation on each data source collection change.
But what about appointment’s data change? We can change Start
property via dialog, Drag-n-Drop and programmatically. In this case, data source collection would not change and scheduler wouldn’t be noticed about necessity of desired height change. But it should be because some appointment’s stack can be increased or decreased during this operation.
To solve this issue, we should subscribe to appointments’ changes and force Height update. I did it inside that intermediate collection that I mentioned above. I do subscription in special method (which is called every time when you add appointment to collection) and notify UI about changes (that can affect desired height) via special property which returns collection itself and is used during binding.
protected override void InsertItem(int index, Appointment item)
{
if (!Contains(item))
{
item.PropertyChanged += onItemPropertyChanged;
base.InsertItem(index, item);
}
}
public void ForceItemPropertyChangedSupportCollectionUpdate()
{
OnPropertyChanged(
new PropertyChangedEventArgs("ItemPropertyChangedSupportCollection"));
}
private void onItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Start")
{
ForceItemPropertyChangedSupportCollectionUpdate();
}
}
Conclusion
This solution successfully works in my project in spite of its hack nature. My future plans are to combine all sources which affect desired height (appointments collection’s change, appointment’s data change, control’s size change, and visible range’s change) via multi binding and try to find a way how to access TimeSlotItemsControl
directly instead of DataTemplate
usage.
Useful Links
History
- 10th November, 2010: Initial post