Есть приложение для отображения данных. Данные представляют родственные сущности, но с некоторыми отличиями у каждой отдельной сущности. (Есть базовая ViewModel и ее специализированные наследники). Данные выводятся в виде стилизованной табличной карточки с отображением пар имя-значение. Для корректного отображения на разных разрешениях (или размерах окна) существует несколько макетов таких табличных карточек (отличаются организацией данных по строках и столбцах). В итоге для решения задачи использовался ListBox для которого задается ItemTemplateSelector в зависимости от его размера. Данное решение на большинстве машин работает так, как ожидалось. Но на некоторых машинах UI поток перестает реагировать на действия пользователя при скроле в ListBox. При этом в диспетчере задач видно, что приложение интенсивно использует ресурсы процессора, т.е. в UI потоке идет некоторая работа. Никаких сообщений о ошибке нет (в том числе и обработанных). Если убрать использование ItemTemplateSelector (вместо ItemTemplateSelector устанавливал ItemTemplate) то данной проблемы не наблюдается. Ниже будет код, иллюстрирующий описанную ситуацию.
<Style x:Key="{x:Type ListBoxItem}" TargetType="ListBoxItem">
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="OverridesDefaultStyle" Value="true" />
<Setter Property="MinHeight" Value="32"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border x:Name="Border" Padding="0,2" SnapsToDevicePixels="True" MinHeight="{TemplateBinding MinHeight}"
Background="{StaticResource ListBoxItem_Background}" Margin="5,5,10,5" CornerRadius="3"
BorderThickness="0,1,0,0" BorderBrush="{StaticResource ListBoxItem_BorderBrush}"
Effect="{StaticResource AppShadowEffect}">
<ContentPresenter />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="ListBox" x:Key="{x:Type ListBox}">
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="FontSize" Value="10"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="Background" Value="{StaticResource ListBoxTemplate_Background}"/>
<Setter Property="Foreground" Value="{StaticResource ListBoxTemplate_Foreground}"/>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
<Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling"/>
<Setter Property="VirtualizingPanel.ScrollUnit" Value="Pixel"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBox">
<Border Name="Border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" HorizontalAlignment="Stretch">
<ScrollViewer Margin="0" Focusable="false" CanContentScroll="True" HorizontalAlignment="Stretch">
<VirtualizingStackPanel Margin="2" IsItemsHost="True"
VirtualizingPanel.IsContainerVirtualizable="True"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.CacheLength="1,1"
Orientation="Vertical" />
</ScrollViewer>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsGrouping" Value="true">
<Setter Property="ScrollViewer.CanContentScroll" Value="false" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Page>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- some other XAML -->
<ListBox Grid.Row="1" ItemsSource="{Binding ItemsSource}" x:Name="listbox">
<ListBox.ItemContainerStyle>
<Style BasedOn="{StaticResource {x:Type ListBoxItem}}" TargetType="ListBoxItem">
<Setter Property="Focusable" Value="False" />
<Setter Property="Background" Value="{StaticResource ListBoxItem_Background}" />
<Setter Property="Margin" Value="10" />
<EventSetter Event="RequestBringIntoView" Handler="IgnoreBringIntoView" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border BorderBrush="{StaticResource ListBoxItem_BorderBrush}"
Background="{TemplateBinding Background}" BorderThickness="1"
CornerRadius="3" Padding="15 0 15 10" x:Name="brdr"
Effect="{StaticResource AppShadowEffect}">
<ContentPresenter />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Background"
Value="{StaticResource ListBoxItem_IsMouseOverBackground}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
<!-- some other XAML -->
</Grid>
</Page>
<local:TemplateSelector x:Key="FiveColumnsTemplateSelector">
<local:TemplateSelector.StringTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" />
</DataTemplate>
</local:TemplateSelector.StringTemplate>
<local:TemplateSelector.FirstTypeTemplate>
<DataTemplate>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Height="40" Orientation="Horizontal" Grid.ColumnSpan="7" VerticalAlignment="Stretch"
Margin="-15 0 -15 10" HorizontalAlignment="Stretch" Background="Transparent">
<Grid Width="22" Height="22" Margin="15 0 0 0">
<Image Width="22" Height="22" Source="{StaticResource IconRes}" />
</Grid>
<TextBlock FontSize="12" Margin="14 0 0 0" VerticalAlignment="Center"
HorizontalAlignment="Left" Text="{Binding Title}"
TextWrapping="NoWrap" FontWeight="Medium" />
</StackPanel>
<TextBlock Text="title 1"
Foreground="{StaticResource TitleForeground}" FontSize="11"
Grid.Row="1" Margin="0 5 10 5" VerticalAlignment="Center" />
<TextBlock Text="{Binding Value1}"
Foreground="{StaticResource ValueForegroundBrush}" FontSize="11"
Grid.Row="2" Margin="0 0 10 5"
FontWeight="SemiBold" />
<Rectangle Grid.Row="1" Grid.RowSpan="2" Grid.Column="0" Width="1"
Fill="{StaticResource DividerBrush}"
HorizontalAlignment="Right" VerticalAlignment="Stretch" />
<!-- other titles and values -->
</Grid>
</DataTemplate>
</local:TemplateSelector.FirstTypeTemplate>
</local:TemplateSelector>
Для ItemTemplateSelector приведена часть разметки, просто в виде примера. Остальные части селектора аналогичны, как и два других ItemTemplateSelector.
public partial class View : Page
{
private static DataTemplateSelector OneColumnTemplate;
private static DataTemplateSelector TwoColumnsTemplate;
private static DataTemplateSelector FiveColumnsTemplate;
private DataTemplateSelector currentTemplate;
static View()
{
OneColumnTemplate = (DataTemplateSelector)Application.Current.TryFindResource("OneColumnTemplateSelector");
TwoColumnsTemplate = (DataTemplateSelector)Application.Current.TryFindResource("TwoColumnsTemplateSelector");
FiveColumnsTemplate = (DataTemplateSelector)Application.Current.TryFindResource("FiveColumnsTemplateSelector");
}
public View()
{
this.InitializeComponent();
this.listbox.SizeChanged += this.OnSizeChanged;
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
DataTemplateSelector t;
if (e.NewSize.Width <= 500d)
{
t = OneColumnTemplate;
}
else if (e.NewSize.Width <= 800d)
{
t = TwoColumnsTemplate;
}
else
{
t = FiveColumnsTemplate;
}
if (object.ReferenceEquals(t, this.currentTemplate))
{
return;
}
this.currentTemplate = t;
this.listbox.ItemTemplateSelector = t;
}
public void IgnoreBringIntoView(object sender, RequestBringIntoViewEventArgs e)
{
e.Handled = true;
}
}
public class TemplateSelector : DataTemplateSelector
{
public DataTemplate StringTemplate { get; set; }
public DataTemplate FirstTypeTemplate { get; set; }
// other DataTemplates
public DataTemplate LastTypeTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
switch (item)
{
case FirstTypeViewModel i:
return this.FirstTypeTemplate;
// other DataTemplates
case LastTypeViewModel i:
return this.LastTypeTemplate;
}
return this.StringTemplate;
}
}
Снова повторю, что на большей части машин поведение кода соответствует ожиданием, т.е. сам подход работоспособен. Из общего у проблемных машин - это ноутбуки на Win10, с разрешением матрицы 1920*1080 (один точно, два с большой вероятностью). Ошибок связанных с отсутствием каких либо библиотек не замечено (лог WinDBG загрузки либ примерно соответствует моей рабочей машине, сообщений с ошибками загрузки не было).
Я предполагаю, что данная проблема связана с обработкой виртуализации внутри списка и/или создания/изменения макета отображения для элемента. Пока описывал проблему появилась идея добавить на старте в ItemsSource по одной записи каждого типа, чтобы инициализировать DataTemplate для каждого типа записи. Посмотрю, что из этого выйдет.
После многих экспериментов, которые не дали результата (висло в любом случае), установил точную причину такой ситуации. Это связано с масштабированием интерфейса (на машине стоял 125 %) и наличием в элементов с разной высотой. Именно при отрисовке элемента другой высоты UI начинает вести интенсивные расчеты. Если на машине выставить масштаб 100%, то все работает как и должно.
Данной проблемы получилось избежать если установить UseLayoutRoundinge="False" для листбокса.
При последующем исследовании с просмотров кода VirtualizingStackPanel нашел, что при скроле вызывается метод SetVerticalOffsetImpl. В нем есть код, который исполняется если используется LayoutRounding. Этот код округляет значение для скрола, базируясь на текущем DPI. Также есть комментарий, который говорит, что возможны бесконечные циклы, если значение скрола будет иметь дробное значение. Я предполагаю, что для масштабирования в 125 % (при масштабировании в 150 % проблема не наблюдается) этот код может давать дробное значение, что в последующем приведет к бесконечному циклу.
Мое текущее решение не использовать LayoutRounding для VirtualizingStackPanel и использовать SnapsToDevicePixels.
private void SetVerticalOffsetImpl(double offset, bool setAnchorInformation)
{
Debug.Assert(IsScrolling, "setting offset on non-scrolling panel");
if (!IsScrolling)
return;
double scrollY = ScrollContentPresenter.ValidateInputOffset(offset, "VerticalOffset");
if (!DoubleUtil.AreClose(scrollY, _scrollData._offset.Y))
{
Vector oldViewportOffset = _scrollData._offset;
// Store the new offset
_scrollData._offset.Y = scrollY;
// Report the change in offset
OnViewportOffsetChanged(oldViewportOffset, _scrollData._offset);
if (IsVirtualizing)
{
InvalidateMeasure();
IsScrollActive = true;
_scrollData.SetVerticalScrollType(oldViewportOffset.Y, scrollY);
// if the scroll is small (at most one screenful), make it an
// anchored scroll if it's not already. Otherwise problems
// can arise when the scroll devirtualizes new content that
// changes the average container size, thus changing the
// effective scroll offset of every item and the total extent;
// SetAndVerifyScrollData would try to maintain the ratio
// offset/extent, which ends up scrolling to a "random" place.
if (!IsVSP45Compat && Orientation == Orientation.Vertical)
{
double delta = Math.Abs(scrollY - oldViewportOffset.Y);
if (DoubleUtil.LessThanOrClose(delta, ViewportHeight))
{
// When item-scrolling, the scroll offset is effectively
// an integer. But certain operations (scroll-to-here,
// drag the thumb) can put fractional values into
// _scrollData. Adding anchoring to such an operation
// produces an infinite loop: the expected distance
// between viewports is fractional, but the actual distance
// is always integral. To fix this, remove the fractions
// here, before setting the anchor.
if (!IsPixelBased)
{
_scrollData._offset.Y = Math.Floor(_scrollData._offset.Y);
_scrollData._computedOffset.Y = Math.Floor(_scrollData._computedOffset.Y);
}
else if (UseLayoutRounding)
{
DpiScale dpi = GetDpi();
// similarly, when layout rounding is enabled, round
// the offsets to pixel boundaries to avoid an infinite
// loop attempting to converge to a fractional offset
_scrollData._offset.Y = UIElement.RoundLayoutValue(_scrollData._offset.Y, dpi.DpiScaleY);
_scrollData._computedOffset.Y = UIElement.RoundLayoutValue(_scrollData._computedOffset.Y, dpi.DpiScaleY);
}
// resolve any ambiguity due to multiple top containers
// (this has already been done in NewItemOffset for
// most anchored scrolls, only small item-scrolls remain)
if (!setAnchorInformation && !IsPixelBased)
{
double topContainerOffset;
FrameworkElement deepestTopContainer = ComputeFirstContainerInViewport(
this, /* viewportElement */
FocusNavigationDirection.Down,
this, /* itemsHost */
null, /* action callback */
true, /* findTopContainer */
out topContainerOffset);
if (topContainerOffset > 0.0)
{
// if there are multiple top containers, reset
// the current offset to agree with the last one
double startingOffset = FindScrollOffset(deepestTopContainer);
_scrollData._computedOffset.Y = startingOffset;
}
}
setAnchorInformation = true;
}
}
}
else if (!IsPixelBased)
{
InvalidateMeasure();
}
else
{
_scrollData._offset.Y = ScrollContentPresenter.CoerceOffset(scrollY, _scrollData._extent.Height, _scrollData._viewport.Height);
_scrollData._computedOffset.Y = _scrollData._offset.Y;
InvalidateArrange();
OnScrollChange();
}
}
if (setAnchorInformation)
{
SetAnchorInformation(isHorizontalOffset:false);
}
if (ScrollTracer.IsEnabled && ScrollTracer.IsTracing(this))
{
ScrollTracer.Trace(this, ScrollTraceOp.SetVOff,
_scrollData._offset, _scrollData._extent, _scrollData._computedOffset);
}
}
this.listbox.ItemTemplateSelectorне должно быть в C# коде вообще. И ваш случай не является специфическим исключением. Адаптивная верстка не на костылях должна делаться, поищите другой способ. Я к сожалению сейчас не могу закопаться в документацию, быть может чуть позже. Но это должно быть просто. – aepot Feb 01 '21 at 17:55Widthпреобразуете в что-то, на что будут реагировать триггеры. Вам даже селектор не понадобится, просто цепляйте нужный шаблон или вообще стиль по дататриггеру, в зависимости от того, сколько там у вас ширины. Кстатиx:Key="{x:Type ListBox}"- ключ - это же просто строка, название стиля, напишитеx:Key="MyListBoxStyle". Вы перепутали немного: можно писатьTargetType="ListBox", а можно с тем же успехомTargetType="{x:Type ListBox}", оба будут одинаково работать.BasedOn="{StaticResource MyListBoxStyle}"– aepot Feb 01 '21 at 18:17DataTemplateнадо возвращать, а статическое значение для триггера, число или строку напримерTwoColumnsFiveColumnsили просто2или5. А триггер будет хватать нужный шаблон. ИDataTemplateздесь вообще не при чем. Вам триггером нужно зацепить нужные сеттеры для стиля всего-лишь. – aepot Feb 01 '21 at 20:46DataTemplate- это привязка разметки к типу данных. То есть совсем для других задач оно предназначено. – aepot Feb 02 '21 at 07:56