Добро пожаловать в форум, Guest  >>   Войти | Регистрация | Поиск | Правила | В избранное | Подписаться
Все форумы / WPF, Silverlight Новый топик    Ответить
 TabControl, ContentTemplate, и DataGrid внутри DataTemplate - как отключить cинхронизацию  [new]
WinterGraveyard
Member

Откуда:
Сообщений: 59
Имеется вот такое окошко:
public partial class MainWindow
{
  public MainWindow()
  {
    InitializeComponent();
    Items = new List<List<TestItem>>
    {
      new List<TestItem>{new TestItem(1), new TestItem(2)},
      new List<TestItem>{new TestItem(3), new TestItem(4)}
    };
    DataContext = this;
  }

  public IReadOnlyCollection<IReadOnlyCollection<TestItem>> Items { get; }
}

public class TestItem
{
  public int Id { get; }
  public string Name { get; }
  public DateTime Date { get; }

  public TestItem(int id)
  {
    Id = id;
    Name = ((char) (64 + id)).ToString();
    Date = DateTime.Now.AddYears(id);
  }
}

public class VisibilityToBoolConverter : IValueConverter
{
  public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  {
    return (Visibility)value == Visibility.Visible;
  }

  public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  {
    return value != null && (bool)value ? Visibility.Visible : Visibility.Collapsed;
  }
}

и вот такой XAML:
<Window
  ....................
  Height="700"
  Width="700">


  <Window.Resources>
    <local:VisibilityToBoolConverter x:Key="VisibilityToBoolConverter" />
    <DataTemplate x:Key="DataTemplate">
      <Border BorderThickness="1" BorderBrush="Black" Margin="3">
        <Grid>
          <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
          </Grid.RowDefinitions>
          <DataGrid
            Margin="3"
            x:Name="DataGrid"
            IsReadOnly="True"
            AutoGenerateColumns="False"
            ItemsSource="{Binding}">
            <DataGrid.Columns>
              <DataGridTextColumn
                Binding="{Binding Id, Mode=OneWay}"
                Header="Id"
                Width="Auto" />
              <DataGridTextColumn
                Binding="{Binding Name, Mode=OneWay}"
                Header="Name"
                Width="Auto" />
              <DataGridTextColumn
                Binding="{Binding Date, Mode=OneWay}"
                Header="Date"
                Width="Auto" />
            </DataGrid.Columns>
          </DataGrid>
          <ListBox
            Grid.Row="1"
            Margin="3"
            ItemsSource="{Binding Columns, ElementName=DataGrid}">
            <ItemsControl.ItemsPanel>
              <ItemsPanelTemplate>
                <StackPanel Orientation="Horizontal" />
              </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemContainerStyle>
              <Style TargetType="{x:Type ListBoxItem}">
                <Setter Property="OverridesDefaultStyle" Value="True" />
                <Setter Property="Template">
                  <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ListBoxItem}">
                      <CheckBox
                        Margin="3"
                        IsChecked="{Binding Visibility, Converter={StaticResource VisibilityToBoolConverter}, Mode=TwoWay}"
                        Content="{Binding Header}" />
                    </ControlTemplate>
                  </Setter.Value>
                </Setter>
              </Style>
            </ItemsControl.ItemContainerStyle>
          </ListBox>
        </Grid>
      </Border>
    </DataTemplate>
  </Window.Resources>

  <TabControl ItemsSource="{Binding Items}" ContentTemplate="{StaticResource DataTemplate}"/>
</Window>

Проблема в том, что состояние DataGrid синхронизируется между вкладками - меняем размер колонки на одной вкладке, переключаемся на другую - он там тоже изменился. Скрываем колонку у одного грида (чекбоксами под гридом) - колонка скрывается у грида на второй вкладке. Причем если TabControl заменить на любой другой ItemsControl - на ListBox, например, то этой синхронизации не наблюдается.
Есть подозрение, что это вызвано тем, что при смене вкладки TabControl делает перегенерацию контента для новой вкладки. Это можно как-то отключить? Если нет, то как вообще можно побороть такое поведение?
4 дек 18, 15:23    [21753084]     Ответить | Цитировать Сообщить модератору
 Re: TabControl, ContentTemplate, и DataGrid внутри DataTemplate - как отключить cинхронизацию  [new]
Roman Mejtes
Member

Откуда: г. Пермь
Сообщений: 3305
<Window x:Class="WpfApp23.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp23"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <TabControl>
            <TabControl.Items>
                <TabItem Header="Tab 1">
                    <Grid> <!-- Седержимое --> </Grid>
                </TabItem>
                <TabItem Header="Tab 2">
                    <Grid> <!-- Седержимое --> </Grid>
                </TabItem>
            </TabControl.Items>
        </TabControl>
    </Grid>
</Window>

Это связано с тем, как работает ContentPresenter, если в качестве Content выступает UIElement, то в качестве содержимого будет отображаться сам элемент. Но 1 элемент не может отображаться одновременно в 2 ContentPresenter'ах.
Если в качестве содержимого (Content) будет выступа не UIElement, а обычный объект или модель представления, то будет использован либо шаблон по умолчанию (TextBlock) или заданный в ContentTemplate или полученный из ContentTemplateSelector.
Так как при переключении вкладок переключает содержимое ContentPresenter'а у TabControl и это не UIElement, то на основе шаблона содержимое формируется каждый раз при переключении. Это может вызывать такие проблемы, как у вас, не только дублирование состояния, но и потерю состояния, которые не хранятся в модели представления (фокус и прочие состояния представления).
Если количество вкладок не предопределено заранее, то пример выше не выход. Придется доработать элемент управления TabControl, нужно создать ContentControl для каждого TabItem'а, поместить туда содержимое вкладки, задать шаблон и сохранить ссылку на этот объект.
При переключении вкладок в качество содержимого должны подставляться ContentControl'ы сохраненные ранее. Так как содержимое сохраненных элементов сформировано, оно не будет перестраиваться.
4 дек 18, 15:42    [21753143]     Ответить | Цитировать Сообщить модератору
 Re: TabControl, ContentTemplate, и DataGrid внутри DataTemplate - как отключить cинхронизацию  [new]
Roman Mejtes
Member

Откуда: г. Пермь
Сообщений: 3305
Вот реализация через мультконвертер. Топорная, это пример на коленке, но рабочий.
Для работы нужно поменять шаблон TabControl'а.

<Window x:Class="WpfApp23.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp23"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainModel/>
    </Window.DataContext>
    <Window.Resources>

        <local:TabItemConverter x:Key="TabItemConverter"/>

        <DataTemplate x:Key="TabTemplate" DataType="{x:Type local:TabModel}">
            <ListBox Margin="5" ItemsSource="{Binding Items}"/>
        </DataTemplate>

        <DataTemplate x:Key="HeaderTemplate" DataType="{x:Type local:TabModel}">
            <TextBlock Text="{Binding Name}"/>
        </DataTemplate>
        
        <SolidColorBrush x:Key="TabItem.Selected.Background" Color="#FFFFFF"/>
        <SolidColorBrush x:Key="TabItem.Selected.Border" Color="#ACACAC"/>
        
        <Style x:Key="TabControlStyle" TargetType="{x:Type TabControl}" BasedOn="{StaticResource {x:Type TabControl}}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TabControl}">
                        <Grid x:Name="templateRoot" ClipToBounds="true" SnapsToDevicePixels="true" KeyboardNavigation.TabNavigation="Local">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition x:Name="ColumnDefinition0"/>
                                <ColumnDefinition x:Name="ColumnDefinition1" Width="0"/>
                            </Grid.ColumnDefinitions>
                            <Grid.RowDefinitions>
                                <RowDefinition x:Name="RowDefinition0" Height="Auto"/>
                                <RowDefinition x:Name="RowDefinition1" Height="*"/>
                            </Grid.RowDefinitions>
                            <TabPanel x:Name="headerPanel" Background="Transparent" Grid.Column="0" IsItemsHost="true" Margin="2,2,2,0" Grid.Row="0" KeyboardNavigation.TabIndex="1" Panel.ZIndex="1"/>
                            <Border x:Name="contentPanel" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Grid.Column="0" KeyboardNavigation.DirectionalNavigation="Contained" Grid.Row="1" KeyboardNavigation.TabIndex="2" KeyboardNavigation.TabNavigation="Local">
                                
                                <ContentPresenter x:Name="PART_SelectedContentHost" 
                                                  Margin="{TemplateBinding Padding}"
                                                  SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">
                                    <ContentPresenter.Content>
                                        <MultiBinding Converter="{StaticResource TabItemConverter}">
                                            <Binding Path="SelectedIndex" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=TabControl}"/>
                                            <Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=TabControl}"/>
                                        </MultiBinding>
                                    </ContentPresenter.Content>
                                </ContentPresenter>
                                
                            </Border>
                        </Grid>
                        <ControlTemplate.Triggers>
                            <Trigger Property="TabStripPlacement" Value="Bottom">
                                <Setter Property="Grid.Row" TargetName="headerPanel" Value="1"/>
                                <Setter Property="Grid.Row" TargetName="contentPanel" Value="0"/>
                                <Setter Property="Height" TargetName="RowDefinition0" Value="*"/>
                                <Setter Property="Height" TargetName="RowDefinition1" Value="Auto"/>
                                <Setter Property="Margin" TargetName="headerPanel" Value="2,0,2,2"/>
                            </Trigger>
                            <Trigger Property="TabStripPlacement" Value="Left">
                                <Setter Property="Grid.Row" TargetName="headerPanel" Value="0"/>
                                <Setter Property="Grid.Row" TargetName="contentPanel" Value="0"/>
                                <Setter Property="Grid.Column" TargetName="headerPanel" Value="0"/>
                                <Setter Property="Grid.Column" TargetName="contentPanel" Value="1"/>
                                <Setter Property="Width" TargetName="ColumnDefinition0" Value="Auto"/>
                                <Setter Property="Width" TargetName="ColumnDefinition1" Value="*"/>
                                <Setter Property="Height" TargetName="RowDefinition0" Value="*"/>
                                <Setter Property="Height" TargetName="RowDefinition1" Value="0"/>
                                <Setter Property="Margin" TargetName="headerPanel" Value="2,2,0,2"/>
                            </Trigger>
                            <Trigger Property="TabStripPlacement" Value="Right">
                                <Setter Property="Grid.Row" TargetName="headerPanel" Value="0"/>
                                <Setter Property="Grid.Row" TargetName="contentPanel" Value="0"/>
                                <Setter Property="Grid.Column" TargetName="headerPanel" Value="1"/>
                                <Setter Property="Grid.Column" TargetName="contentPanel" Value="0"/>
                                <Setter Property="Width" TargetName="ColumnDefinition0" Value="*"/>
                                <Setter Property="Width" TargetName="ColumnDefinition1" Value="Auto"/>
                                <Setter Property="Height" TargetName="RowDefinition0" Value="*"/>
                                <Setter Property="Height" TargetName="RowDefinition1" Value="0"/>
                                <Setter Property="Margin" TargetName="headerPanel" Value="0,2,2,2"/>
                            </Trigger>
                            <Trigger Property="IsEnabled" Value="false">
                                <Setter Property="TextElement.Foreground" TargetName="templateRoot" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        
    </Window.Resources>
    <Grid>
        <TabControl ItemsSource="{Binding Tabs}"
                    ContentTemplate="{StaticResource TabTemplate}"
                    ItemTemplate="{StaticResource HeaderTemplate}" Style="{DynamicResource TabControlStyle}"/>
    </Grid>
</Window>


public class TabItemConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values.Length == 2 && values[0] is int selectedIndex && values[1] is TabControl tabControl)
        {
            var tabItem = tabControl.ItemContainerGenerator.ContainerFromIndex(selectedIndex) as TabItem;
            if (tabItem != null)
            {
                if (tabItem.Tag == null)
                {
                    var control = new ContentControl { Content = tabItem.Content, ContentTemplate = tabControl.ContentTemplate };
                    tabItem.Tag = control;
                }
                return tabItem.Tag;
            }
        }
        return null;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

модель для примера и роли не играет
public class TabModel
{
    public TabModel(string name)
    {
        Name = name;
        var rnd = new Random();
        var count = rnd.Next(100, 500);
        Items = new List<string>(count);
        for (var i = 0; i < count; i++)
            Items.Add($"{Name} - Item #{i}");
    }

    public string Name { get; }
    public List<string> Items { set; get; }
}

public class MainModel
{
    public MainModel()
    {
        Tabs = new List<TabModel>(5)
        {
            new TabModel("Tab1"),
            new TabModel("Tab2"),
            new TabModel("Tab3"),
            new TabModel("Tab4"),
            new TabModel("Tab5"),
        };
    }

    public List<TabModel> Tabs { set; get; }
}
4 дек 18, 16:29    [21753300]     Ответить | Цитировать Сообщить модератору
 Re: TabControl, ContentTemplate, и DataGrid внутри DataTemplate - как отключить cинхронизацию  [new]
WinterGraveyard
Member

Откуда:
Сообщений: 59
Роман, спасибо.
Погуглил - оказывается, проблема старая, и тянется чуть ли не с 2007-го года. Ситуация в точности противоположна моему предположению: для сгенерированных TabItems по ContentTemplate TabControl не пересоздает контент, а только меняет data item из ItemsSource, и всё то, что не связано с VM, остается одинаковым для всех вкладок (как вы писали выше).
Нашел несколько решений:
https://stackoverflow.com/questions/9794151/stop-tabcontrol-from-recreating-its-children
https://www.codeproject.com/Articles/460989/WPF-TabControl-Turning-Off-Tab-Virtualization
- остановлюсь, наверное, на последнем (оно вполне работает, я проверил).
5 дек 18, 08:30    [21753909]     Ответить | Цитировать Сообщить модератору
 Re: TabControl, ContentTemplate, и DataGrid внутри DataTemplate - как отключить cинхронизацию  [new]
Roman Mejtes
Member

Откуда: г. Пермь
Сообщений: 3305
WinterGraveyard
Роман, спасибо.
Погуглил - оказывается, проблема старая, и тянется чуть ли не с 2007-го года. Ситуация в точности противоположна моему предположению: для сгенерированных TabItems по ContentTemplate TabControl не пересоздает контент, а только меняет data item из ItemsSource, и всё то, что не связано с VM, остается одинаковым для всех вкладок (как вы писали выше).
Нашел несколько решений:
https://stackoverflow.com/questions/9794151/stop-tabcontrol-from-recreating-its-children
https://www.codeproject.com/Articles/460989/WPF-TabControl-Turning-Off-Tab-Virtualization
- остановлюсь, наверное, на последнем (оно вполне работает, я проверил).

разберитесь как работает ContentPresenter, что и как он отображает и тогда вопросов таких не будет.
подобной "магии" в WPF полно, и когда не знаешь, как это работает, очень тяжко. Магия WPF это палка о двух концах.
5 дек 18, 14:16    [21754375]     Ответить | Цитировать Сообщить модератору
Все форумы / WPF, Silverlight Ответить