2

Есть приложение для отображения данных. Данные представляют родственные сущности, но с некоторыми отличиями у каждой отдельной сущности. (Есть базовая 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"/>
    &lt;Setter Property=&quot;Template&quot;&gt;
        &lt;Setter.Value&gt;
            &lt;ControlTemplate TargetType=&quot;ListBoxItem&quot;&gt;
                &lt;Border x:Name=&quot;Border&quot; Padding=&quot;0,2&quot; SnapsToDevicePixels=&quot;True&quot; MinHeight=&quot;{TemplateBinding MinHeight}&quot;
                        Background=&quot;{StaticResource ListBoxItem_Background}&quot; Margin=&quot;5,5,10,5&quot; CornerRadius=&quot;3&quot;
                        BorderThickness=&quot;0,1,0,0&quot; BorderBrush=&quot;{StaticResource ListBoxItem_BorderBrush}&quot;
                        Effect=&quot;{StaticResource AppShadowEffect}&quot;&gt;
                    &lt;ContentPresenter /&gt;
                &lt;/Border&gt;
            &lt;/ControlTemplate&gt;
        &lt;/Setter.Value&gt;
    &lt;/Setter&gt;
&lt;/Style&gt;

&lt;Style TargetType=&quot;ListBox&quot; x:Key=&quot;{x:Type ListBox}&quot;&gt;
    &lt;Setter Property=&quot;OverridesDefaultStyle&quot; Value=&quot;True&quot;/&gt;
    &lt;Setter Property=&quot;SnapsToDevicePixels&quot; Value=&quot;True&quot;/&gt;
    &lt;Setter Property=&quot;BorderThickness&quot; Value=&quot;0&quot;/&gt;
    &lt;Setter Property=&quot;FontSize&quot; Value=&quot;10&quot;/&gt;
    &lt;Setter Property=&quot;FocusVisualStyle&quot; Value=&quot;{x:Null}&quot; /&gt;

    &lt;Setter Property=&quot;Background&quot; Value=&quot;{StaticResource ListBoxTemplate_Background}&quot;/&gt;
    &lt;Setter Property=&quot;Foreground&quot; Value=&quot;{StaticResource ListBoxTemplate_Foreground}&quot;/&gt;

    &lt;Setter Property=&quot;ScrollViewer.HorizontalScrollBarVisibility&quot; Value=&quot;Disabled&quot;/&gt;
    &lt;Setter Property=&quot;VirtualizingPanel.VirtualizationMode&quot; Value=&quot;Recycling&quot;/&gt;
    &lt;Setter Property=&quot;VirtualizingPanel.ScrollUnit&quot; Value=&quot;Pixel&quot;/&gt;

    &lt;Setter Property=&quot;Template&quot;&gt;
        &lt;Setter.Value&gt;
            &lt;ControlTemplate TargetType=&quot;ListBox&quot;&gt;
                &lt;Border Name=&quot;Border&quot; Background=&quot;{TemplateBinding Background}&quot; BorderBrush=&quot;{TemplateBinding BorderBrush}&quot; 
                        BorderThickness=&quot;{TemplateBinding BorderThickness}&quot; HorizontalAlignment=&quot;Stretch&quot;&gt;
                    &lt;ScrollViewer Margin=&quot;0&quot; Focusable=&quot;false&quot; CanContentScroll=&quot;True&quot; HorizontalAlignment=&quot;Stretch&quot;&gt;
                        &lt;VirtualizingStackPanel Margin=&quot;2&quot; IsItemsHost=&quot;True&quot; 
                                                VirtualizingPanel.IsContainerVirtualizable=&quot;True&quot;
                                                VirtualizingPanel.IsVirtualizing=&quot;True&quot;
                                                VirtualizingPanel.CacheLength=&quot;1,1&quot;
                                                Orientation=&quot;Vertical&quot; /&gt;
                    &lt;/ScrollViewer&gt;
                &lt;/Border&gt;

                &lt;ControlTemplate.Triggers&gt;
                    &lt;Trigger Property=&quot;IsGrouping&quot; Value=&quot;true&quot;&gt;
                        &lt;Setter Property=&quot;ScrollViewer.CanContentScroll&quot; Value=&quot;false&quot; /&gt;
                    &lt;/Trigger&gt;
                &lt;/ControlTemplate.Triggers&gt;
            &lt;/ControlTemplate&gt;
        &lt;/Setter.Value&gt;
    &lt;/Setter&gt;
&lt;/Style&gt;

&lt;Page&gt; 
    &lt;Grid&gt;  
        &lt;Grid.RowDefinitions&gt;
            &lt;RowDefinition Height=&quot;Auto&quot; /&gt;
            &lt;RowDefinition Height=&quot;*&quot; /&gt;
        &lt;/Grid.RowDefinitions&gt;

    &lt;!-- some other XAML --&gt;   

        &lt;ListBox Grid.Row=&quot;1&quot; ItemsSource=&quot;{Binding ItemsSource}&quot; x:Name=&quot;listbox&quot;&gt;
            &lt;ListBox.ItemContainerStyle&gt;
                &lt;Style BasedOn=&quot;{StaticResource {x:Type ListBoxItem}}&quot; TargetType=&quot;ListBoxItem&quot;&gt;
                    &lt;Setter Property=&quot;Focusable&quot; Value=&quot;False&quot; /&gt;
                    &lt;Setter Property=&quot;Background&quot; Value=&quot;{StaticResource ListBoxItem_Background}&quot; /&gt;
                    &lt;Setter Property=&quot;Margin&quot; Value=&quot;10&quot; /&gt;
                    &lt;EventSetter Event=&quot;RequestBringIntoView&quot; Handler=&quot;IgnoreBringIntoView&quot; /&gt;

                    &lt;Setter Property=&quot;Template&quot;&gt;
                        &lt;Setter.Value&gt;
                            &lt;ControlTemplate TargetType=&quot;ListBoxItem&quot;&gt;
                                &lt;Border BorderBrush=&quot;{StaticResource ListBoxItem_BorderBrush}&quot;
                                        Background=&quot;{TemplateBinding Background}&quot; BorderThickness=&quot;1&quot;
                                        CornerRadius=&quot;3&quot; Padding=&quot;15 0 15 10&quot; x:Name=&quot;brdr&quot;
                                        Effect=&quot;{StaticResource AppShadowEffect}&quot;&gt;
                                    &lt;ContentPresenter /&gt;
                                &lt;/Border&gt;

                                &lt;ControlTemplate.Triggers&gt;
                                    &lt;Trigger Property=&quot;IsMouseOver&quot; Value=&quot;true&quot;&gt;
                                        &lt;Setter Property=&quot;Background&quot;
                                                Value=&quot;{StaticResource ListBoxItem_IsMouseOverBackground}&quot; /&gt;
                                    &lt;/Trigger&gt;
                                &lt;/ControlTemplate.Triggers&gt;
                            &lt;/ControlTemplate&gt;
                        &lt;/Setter.Value&gt;
                    &lt;/Setter&gt;
                &lt;/Style&gt;
            &lt;/ListBox.ItemContainerStyle&gt;
        &lt;/ListBox&gt;

        &lt;!-- some other XAML --&gt;   
    &lt;/Grid&gt;
&lt;/Page&gt;

&lt;local:TemplateSelector x:Key=&quot;FiveColumnsTemplateSelector&quot;&gt;
    &lt;local:TemplateSelector.StringTemplate&gt;
        &lt;DataTemplate&gt;
            &lt;TextBlock Text=&quot;{Binding}&quot; /&gt;
        &lt;/DataTemplate&gt;
    &lt;/local:TemplateSelector.StringTemplate&gt;

    &lt;local:TemplateSelector.FirstTypeTemplate&gt;
        &lt;DataTemplate&gt;
            &lt;Grid HorizontalAlignment=&quot;Stretch&quot; VerticalAlignment=&quot;Center&quot;&gt;
                &lt;Grid.RowDefinitions&gt;
                    &lt;RowDefinition Height=&quot;Auto&quot; /&gt;
                    &lt;RowDefinition Height=&quot;Auto&quot; /&gt;
                    &lt;RowDefinition Height=&quot;Auto&quot; /&gt;
                &lt;/Grid.RowDefinitions&gt;

                &lt;Grid.ColumnDefinitions&gt;
                    &lt;ColumnDefinition Width=&quot;*&quot; /&gt;
                    &lt;ColumnDefinition Width=&quot;*&quot; /&gt;
                    &lt;ColumnDefinition Width=&quot;*&quot; /&gt;
                    &lt;ColumnDefinition Width=&quot;*&quot; /&gt;
                    &lt;ColumnDefinition Width=&quot;*&quot; /&gt;
                    &lt;ColumnDefinition Width=&quot;*&quot; /&gt;
                    &lt;ColumnDefinition Width=&quot;*&quot; /&gt;
                &lt;/Grid.ColumnDefinitions&gt;

                &lt;StackPanel Height=&quot;40&quot; Orientation=&quot;Horizontal&quot; Grid.ColumnSpan=&quot;7&quot; VerticalAlignment=&quot;Stretch&quot;
                            Margin=&quot;-15 0 -15 10&quot; HorizontalAlignment=&quot;Stretch&quot; Background=&quot;Transparent&quot;&gt;
                    &lt;Grid Width=&quot;22&quot; Height=&quot;22&quot; Margin=&quot;15 0 0 0&quot;&gt;
                        &lt;Image Width=&quot;22&quot; Height=&quot;22&quot; Source=&quot;{StaticResource IconRes}&quot; /&gt;

                    &lt;/Grid&gt;

                    &lt;TextBlock FontSize=&quot;12&quot; Margin=&quot;14 0 0 0&quot; VerticalAlignment=&quot;Center&quot;
                                           HorizontalAlignment=&quot;Left&quot; Text=&quot;{Binding Title}&quot;
                                           TextWrapping=&quot;NoWrap&quot; FontWeight=&quot;Medium&quot; /&gt;
                &lt;/StackPanel&gt;

                &lt;TextBlock Text=&quot;title 1&quot;
                           Foreground=&quot;{StaticResource TitleForeground}&quot; FontSize=&quot;11&quot;
                                       Grid.Row=&quot;1&quot; Margin=&quot;0 5 10 5&quot; VerticalAlignment=&quot;Center&quot; /&gt;

                &lt;TextBlock Text=&quot;{Binding Value1}&quot;
                           Foreground=&quot;{StaticResource ValueForegroundBrush}&quot; FontSize=&quot;11&quot;
                           Grid.Row=&quot;2&quot; Margin=&quot;0 0 10 5&quot;
                           FontWeight=&quot;SemiBold&quot; /&gt;

                &lt;Rectangle Grid.Row=&quot;1&quot; Grid.RowSpan=&quot;2&quot; Grid.Column=&quot;0&quot; Width=&quot;1&quot;
                           Fill=&quot;{StaticResource DividerBrush}&quot;
                           HorizontalAlignment=&quot;Right&quot; VerticalAlignment=&quot;Stretch&quot; /&gt;

                &lt;!-- other titles and values --&gt;
            &lt;/Grid&gt;
        &lt;/DataTemplate&gt;
    &lt;/local:TemplateSelector.FirstTypeTemplate&gt;        
&lt;/local:TemplateSelector&gt;

Для 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(&quot;OneColumnTemplateSelector&quot;);
    TwoColumnsTemplate = (DataTemplateSelector)Application.Current.TryFindResource(&quot;TwoColumnsTemplateSelector&quot;);
    FiveColumnsTemplate = (DataTemplateSelector)Application.Current.TryFindResource(&quot;FiveColumnsTemplateSelector&quot;);
}

public View()
{
    this.InitializeComponent();
    this.listbox.SizeChanged += this.OnSizeChanged;
}

private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
    DataTemplateSelector t;
    if (e.NewSize.Width &lt;= 500d)
    {
        t = OneColumnTemplate;
    }
    else if (e.NewSize.Width &lt;= 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, &quot;VerticalOffset&quot;);
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 &quot;random&quot; place.
        if (!IsVSP45Compat &amp;&amp; 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 &amp;&amp; !IsPixelBased)
                {
                    double topContainerOffset;
                    FrameworkElement deepestTopContainer = ComputeFirstContainerInViewport(
                        this,   /* viewportElement */
                        FocusNavigationDirection.Down,
                        this,   /* itemsHost */
                        null,   /* action callback */
                        true,   /* findTopContainer */
                        out topContainerOffset);

                    if (topContainerOffset &gt; 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 &amp;&amp; ScrollTracer.IsTracing(this))
{
    ScrollTracer.Trace(this, ScrollTraceOp.SetVOff,
        _scrollData._offset, _scrollData._extent, _scrollData._computedOffset);
}

}

  • Ну добавьте логирование, найдите что же там такое делает процессор и устраните. – tym32167 Feb 01 '21 at 16:07
  • Есть еще всякие профайлеры, попробуйте их использовать чтобы найти узкие место в вашем коде. – tym32167 Feb 01 '21 at 16:08
  • 1
    Судя по поведению, вы попадаете в бесконечный цикл. А вот где - не ясно. Но работа у вас с селектором действительно странная, вы подсовывете контролу не шаблоны, а селекторы. А ведь именно селектор должен подсовывать шаблон. Выглядит как большой сложный костыль, должно быть проще. Пока не знаю, как это в порядок привести. – aepot Feb 01 '21 at 16:20
  • @aepot, т.е. изменять селектор шаблонов не очень хорошая идея? Я это для себя представляю просто как еще один уровень кастомизации. Тоже думал, что попадаю в бесконечный цикл. Правда никак не могу понять, почему проблема только на некоторых машинах воспроизводится. – Виталий Ефимов Feb 01 '21 at 17:40
  • 1
    @ВиталийЕфимов я уверен, что есть более простой и гуманный способ решить данную задачу. В идеале работы с контролами типа this.listbox.ItemTemplateSelector не должно быть в C# коде вообще. И ваш случай не является специфическим исключением. Адаптивная верстка не на костылях должна делаться, поищите другой способ. Я к сожалению сейчас не могу закопаться в документацию, быть может чуть позже. Но это должно быть просто. – aepot Feb 01 '21 at 17:55
  • @aepot, мой ход мысли был таким: выберем ItemTemplateSelector под текущий размер окна приложения, а он уже раздаст шаблоны по типам. Этот функционал изначально был мной сделан через XAML и конвертер, но такое решение более удобным посчитал. Одна из идей - вернуться к решению без "code behind". Завтра буду смотреть. Спасибо. – Виталий Ефимов Feb 01 '21 at 18:06
  • 1
    Вот да, с ходу у самого мысли про конвертер. В котором к примеру вы привязку к Width преобразуете в что-то, на что будут реагировать триггеры. Вам даже селектор не понадобится, просто цепляйте нужный шаблон или вообще стиль по дататриггеру, в зависимости от того, сколько там у вас ширины. Кстати x:Key="{x:Type ListBox}" - ключ - это же просто строка, название стиля, напишите x:Key="MyListBoxStyle". Вы перепутали немного: можно писать TargetType="ListBox", а можно с тем же успехом TargetType="{x:Type ListBox}", оба будут одинаково работать. BasedOn="{StaticResource MyListBoxStyle}" – aepot Feb 01 '21 at 18:17
  • @aepot, я перешёл от XAML и конвертера с следующих соображений: ширина у нас меняется непрерывно, что приводит к постоянному вызову конвертера и работы последующей логики. А так, как сделано сейчас, логика изменений работает только при переходе некоторых точек ширины. – Виталий Ефимов Feb 01 '21 at 18:56
  • 1
    @ВиталийЕфимов какая разница где код проверки ширины отработает, что в конвертере оно постоянно вызывается, что в в обработчике, одно и то же в этом плане. Триггер не будет ничего менять, если значение свойства, вернувшегося из конвертера не изменилось. WPF неплохо так оптимизирован. Так что в плане оптимизаций - разницы нет. Если конечно конвертер реализован без наворотов и по-простому. - Вот оно – aepot Feb 01 '21 at 19:12
  • @aepot, спасибо. Одна из идей, которые хочу проверить - возвращать из конвертера DataTemplate базируясь на ширине и типе ViewModel. Правда тут нужно привязывать темплит самого листбокс итема и XAML немного несуразный получается (нужно привязать данные для конвертера(ширину листбокса) через RelativeSource). – Виталий Ефимов Feb 01 '21 at 20:40
  • 1
    Вам не DataTemplate надо возвращать, а статическое значение для триггера, число или строку например TwoColumns FiveColumns или просто 2 или 5. А триггер будет хватать нужный шаблон. И DataTemplate здесь вообще не при чем. Вам триггером нужно зацепить нужные сеттеры для стиля всего-лишь. – aepot Feb 01 '21 at 20:46
  • @aepot, Я понимаю. Но с конвертером, возвращающим DataTemplate мне код кажется более аккуратным и удобочитаемым. Но еще подумаю над этим. Еще есть вариант раздавать через дататригер темплитселектор (так раньше было). Спасибо. – Виталий Ефимов Feb 02 '21 at 07:33
  • Вам надо не на код смотреть, а на то, как это работает. DataTemplate - это привязка разметки к типу данных. То есть совсем для других задач оно предназначено. – aepot Feb 02 '21 at 07:56

1 Answers1

2

Вы используете DataTemplate и DataTemplateSelector не по назначению. Какое поведение в этом случае можно ожидать от приложения, я не знаю, возможно и зависания. Фактически вы вмешиваетесь низкоуровнево в логику работы WPF, а там даже если исходники на гитхабе почитать, не всё так однозначно понятно. Лучше использовать механизмы для реализации адаптивной верстки, доступные разработчику штатно, а не выдумывать их самостоятельно.

В WPF для реализации адаптивной верстки к сожалению нет прямого инструмента, как например AdaptiveTrigger в UWP. Реализуется адаптив следующим образом:

  1. Создаёте конвертер, который возвращает bool в зависимости от передаваемого в него значения. Если значение меньше числа, переданного в параметр, вернуть true, иначе false.
public class AdaptiveConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        => value is double measure && parameter is string s && double.TryParse(s, NumberStyles.Float, culture.NumberFormat, out double threshold) && measure <= threshold;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    =&gt; null;

}

  1. Подключаете конвертер в ресурсы
<Window.Resources>
    <local:AdaptiveConverter x:Key="AdaptiveConverter"/>
</Window.Resources>
  1. И используете в триггерах, например
<Style TargetType="{x:Type TextBlock}" x:Key="MyTextBlockStyle">
    <Setter Property="FontSize" Value="40"/>
    <Style.Triggers>
        <DataTrigger Binding="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=Window}, Converter={StaticResource AdaptiveConverter}, ConverterParameter=800}" Value="True">
            <Setter Property="FontSize" Value="20"/>
        </DataTrigger>
    </Style.Triggers>
</Style>

Биндинг выглядит немного монструозно, но количество сеттеров в одном триггере может быть любое. То есть на одно пороговое значение вам нужен только один триггер.

<TextBlock Text="Test" Style="{StaticResource MyTextBlockStyle}"/>

Вот, собственно и всё, при значении ActualWidth окна больше 800 размер шрифта будет 40, иначе 20.

Я проверял, ничего не зависает.

aepot
  • 49,560
  • Да, я понял Ваш подход. Но все же пока попробую использовать DataTemplate, но без DataTemplateSelector, так как часть стиля у данных карточек общая и ее удобно указать через ListBox.ItemContainerStyle, а отличия есть в данных и их дизайне, поэтому их буду указывать в ListBox.ItemTemplate с использованием конвертера. Зависание происходит только на некоторых машинах, возможно на Вашей было бы все хорошо и с DataTemplateSelector. Спасибо. – Виталий Ефимов Feb 02 '21 at 09:12
  • 1
    @ВиталийЕфимов В Style можно добавить Template, то есть стиль может определять не только свойства, но и разметку контрола. Например в ItemContainerStyle вы можете вставить DataTemplate, содержащий Grid, а разметку Grid определить отдельно с использованием ControlTemplate. Если не пробовали такой способ разметки, попробуйте, быть может он в каких-то местах вам покажется удобнее. – aepot Feb 02 '21 at 09:28
  • Я нашел точную причину зависания, дописал в пост. Получилось сделать зависание на ноутбуке, на котором при масштабе в 100 % все работало правильно. – Виталий Ефимов Feb 02 '21 at 12:23
  • @ВиталийЕфимов ну ок, 125% - причина. Но виснет то почему? Не должно. У вас там миллион контролов генерится? Ограничьте количество отображаемых данных, например вот так. Потом если вы явно зададите размер шрифта, то интерфейсу чихать будет на то, что там настроено в системе. – aepot Feb 02 '21 at 12:37
  • У меня до 10 итемов отображается. Зависание идет когда следующий отображаемый элемент имеет другую высоту, чем предыдущие. Это приводит к тому что WPF интенсивно вычисляет как разместить этот контрол и это подвешивает UI поток. – Виталий Ефимов Feb 02 '21 at 12:40
  • @ВиталийЕфимов Интенсивно вычислять там нечего, надо искать баг в верстке или логике приложения. – aepot Feb 02 '21 at 12:42
  • 1
    Связанный топик: https://ru.stackoverflow.com/a/981982/218063 – Андрей NOP Feb 02 '21 at 13:57
  • Получилось убрать зависание UI потока без изменения кода и разметки. Если установить для листбокса UseLayoutRounding="False". Сама причина этого пока не ясна. – Виталий Ефимов Feb 03 '21 at 08:13
  • @ВиталийЕфимов еще у ListBox можете добавить SnapToDevicePixels="True", если оно немного размазывает границы. – aepot Feb 03 '21 at 08:15