I'm making a user control for a data grid. This data grid has some old and new entries. It is required that the new entries are shown at the top of the screen yet the order of data has to be chronological. So I am pushing the whole grid upwards by adding empty rows. For example if I have 8 old entries and 2 new entries, the 8 old entries have to be "hidden" above the first of the two new entries.
To calculate the empty rows, I want to divide the actual height of the user control by the row height. I have found a way to get the actual height through a dependency property (see code below). I need it available at the start, so I implemented a trigger to give me acces to the load event. Yet at this time the user control still has a size of 0. So is there another event that would allow me to calculate this on the correct timing?
A last note, I am using Galasoft MVVMLight and a matching mvvm-pattern. Also the dependency property does give the right height once you change the height of the window after first initialization.
The view
<DataGrid x:Class="Kwa.Presentation.Views.AlarmList.AlarmList"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Kwa.Presentation.Views.AlarmList"
xmlns:behaviors="clr-namespace:Kwa.Presentation.Behaviors"
xmlns:Trans="clr-namespace:Kwa.Presentation.Resources"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
mc:Ignorable="d"
d:DesignHeight="500" d:DesignWidth="750"
ItemsSource="{Binding Alarms}"
SelectedItem="{Binding SelectedAlarm, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectionChanged = "AlarmFramework_SelectionChanged"
IsSynchronizedWithCurrentItem="True"
CanUserResizeColumns="True" IsReadOnly="True" CanUserReorderColumns="False" CanUserSortColumns="False" SelectionMode="Single" CanUserAddRows="False"
Background="White" RowHeaderWidth="0" AutoGenerateColumns="False" GridLinesVisibility="None" RowHeight="20" FrozenColumnCount = "1"
ScrollViewer.CanContentScroll="False" ScrollViewer.VerticalScrollBarVisibility="Auto" ScrollViewer.HorizontalScrollBarVisibility="Auto"
x:Name="AlarmFramework"
behaviors:ElementActualSizeBehavior.ActualHeight="{Binding ListHeight}"
>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding AlarmListLoadedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<DataGrid.Columns>
<DataGridTemplateColumn Header="{x:Static Trans:TranslatedResources.AlarmColumnHeaderTime}" Width="auto" HeaderStyle="{StaticResource WithButt}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<components:FlagControl VerticalAlignment="Center" Height="15" Width="15" FlagColor="{Binding Severity, Converter={StaticResource AlarmSeverityToColorConverter}}"
Visibility="{Binding AckStatus, Converter={StaticResource InvertedBoolToVisibilityConverter}, Mode=TwoWay}"/>
<TextBlock Text="{Binding DateTimeString}" Padding="10,0,0,0" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="{x:Static Trans:TranslatedResources.AlarmColumnHeaderSeverity}" Binding="{Binding SeverityString}" Width="auto" HeaderStyle="{StaticResource WithoutButt}"/>
<DataGridTextColumn Header="{x:Static Trans:TranslatedResources.AlarmColumnHeaderDescription}" Binding="{Binding Description}" d:Width="400" Width="*" HeaderStyle="{StaticResource WithoutButt}"/>
</DataGrid.Columns>
</DataGrid>
Dependency property to get actual height
public class ElementActualSizeBehavior
{
public static double GetActualWidth(DependencyObject obj)
{
return (double)obj.GetValue(ActualWidthProperty);
}
public static void SetActualWidth(DependencyObject obj, double value)
{
obj.SetValue(ActualWidthProperty, value);
}
public static double GetActualHeight(DependencyObject obj)
{
return (double)obj.GetValue(ActualHeightProperty);
}
public static void SetActualHeight(DependencyObject obj, double value)
{
obj.SetValue(ActualHeightProperty, value);
}
public static readonly DependencyProperty ActualWidthProperty = DependencyProperty.RegisterAttached("ActualWidth", typeof(double), typeof(ElementActualSizeBehavior), new FrameworkPropertyMetadata(double.NaN, new PropertyChangedCallback(ActualWidthChanged)) { BindsTwoWayByDefault = true });
public static readonly DependencyProperty ActualHeightProperty = DependencyProperty.RegisterAttached("ActualHeight", typeof(double), typeof(ElementActualSizeBehavior), new FrameworkPropertyMetadata(double.NaN, new PropertyChangedCallback(ActualHeightChanged)) { BindsTwoWayByDefault = true });
private static void ActualHeightChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
Control w = source as Control;
if (w != null)
{
w.SizeChanged += (se, ev) =>
{
SetActualHeight((DependencyObject)se, ev.NewSize.Height);
};
}
}
private static void ActualWidthChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
Control w = source as Control;
if (w != null)
{
w.SizeChanged += (se, ev) =>
{
SetActualWidth((DependencyObject)se, ev.NewSize.Width);
};
}
}
}
The view model
public class AlarmListViewModel : MainViewModelBase
{
#region Properties
private double _listHeight;
public double ListHeight
{
get { return _listHeight; }
set
{
if (Double.IsNaN(value))
{
//Debug.WriteLine("nan");
return;
}
_listHeight = value;
//Debug.WriteLine(value);
RaisePropertyChanged(() => ListHeight);
}
}
private ObservableCollection<AlarmEntryViewModel> _alarms;
public ObservableCollection<AlarmEntryViewModel> Alarms
{
get { return _alarms; }
set { Set(() => Alarms, ref _alarms, value); }
}
private AlarmEntryViewModel _selectedAlarm;
public AlarmEntryViewModel SelectedAlarm
{
get { return _selectedAlarm; }
set { Set(() => SelectedAlarm, ref _selectedAlarm, value); }
}
private int _acknowledgeAllowed;
public int AcknowledgeAllowed
{
get { return _acknowledgeAllowed; }
set { Set(() => AcknowledgeAllowed, ref _acknowledgeAllowed, value); }
}
private int _acknowledgableAlarms;
public int AcknowledgableAlarms
{
get { return _acknowledgableAlarms; }
set { Set(() => AcknowledgableAlarms, ref _acknowledgableAlarms, value); }
}
private int _rowHeight;
public int RowHeight
{
get { return _rowHeight; }
set { Set(() => RowHeight, ref _rowHeight, value); }
}
private readonly IActionCommand _acknowledgeCommand;
public IActionCommand AcknowledgeCommand
{
get { return _acknowledgeCommand; }
}
private readonly IActionCommand _alarmListLoadedCommand;
public IActionCommand AlarmListLoadedCommand
{
get { return _alarmListLoadedCommand; }
}
public int MaxAcknowledgedAlarm;
public int AlarmToSendIndex { get; set; }
#endregion
#region Constructor
public AlarmListViewModel()
{
//Lock collection to stop inconsistent item source exception
Alarms = new ObservableCollection<AlarmEntryViewModel>();
BindingOperations.EnableCollectionSynchronization(Alarms, _AlarmsLock);
//Add command
_acknowledgeCommand = new ActionCommand<AlarmEntryViewModel>(p => Acknowledge(p));
_alarmListLoadedCommand = new ActionCommand<AlarmListViewModel>(p => Startup());
}
#endregion
#region Private Methods
private void Startup()
{
var rowsInView = (int)Math.Floor(ListHeight / RowHeight);
var rowsInAlarms = Alarms.Count;
var rowsNeeded = rowsInView - rowsInAlarms;
}
Eventually somebody explained the problem to me and gave a solution. The problem lied into the fact that the user control's height / width doesn't actually change at load time. It stays relative to it's ancestors width / height (Or something like that). Anyhow, that's why you don't get an update in you view model.
The solution exists out of two parts. First the ElementActionSizeBehavior has to be implemented as in the code above. Then you have to explicitly declare the components size. This can be done by adding the line:
So the updated view header should be something like this:
You also do not need the interactivity parts anymore.
Hopefully this can help other people as well getting their actual heights and widths.