Как мне представить элементы управления TabItem в WPF? - PullRequest
7 голосов
/ 03 февраля 2010

C # XBap Application

У меня есть TabControl с четырьмя TabItems.Два из этих TabItems просто содержат DataGrid из WPFToolkit, который извлекает довольно маленький набор данных (100 строк на 4 столбца) из базы данных SQL Server.Моя проблема в том, что когда я загружаю свое приложение и нажимаю на один из элементов TabItems, содержащих сетку данных.Мне кажется, что пауза составляет 2-3 секунды, прежде чем она фокусируется на этой вкладке.Это происходит только при первом нажатии на вкладку.Кажется, это рендеринг сетки данных.

Как сделать так, чтобы эти вкладки отображались предварительно при загрузке приложения, чтобы при нажатии пользователем вкладки не было начальной паузы в 2-3 секунды, прежде чем вкладка появится.

Ответы [ 4 ]

6 голосов
/ 18 апреля 2012

Мы используем стандартный WPF TabControl, и проблема в том, что он очищает VisualTree каждый раз, когда вы меняете SelectedItem.

В итоге мы создали специальный TabControl (я назвал его TabControlEx), который сохраняет все элементы отображаемыми, но выбирает просто показать / скрыть ContentPresenters для TabItems.

Вот соответствующий код

using System;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;

namespace MVVM.Demo
{
    /// <summary>
    /// The standard WPF TabControl is quite bad in the fact that it only
    /// even contains the current TabItem in the VisualTree, so if you
    /// have complex views it takes a while to re-create the view each tab
    /// selection change.Which makes the standard TabControl very sticky to
    /// work with. This class along with its associated ControlTemplate
    /// allow all TabItems to remain in the VisualTree without it being Sticky.
    /// It does this by keeping all TabItem content in the VisualTree but
    /// hides all inactive TabItem content, and only keeps the active TabItem
    /// content shown.
    /// </summary>
    [TemplatePart(Name = "PART_ItemsHolder", Type = typeof(Panel))]
    public class TabControlEx : TabControl
    {
        #region Data
        private Panel itemsHolder = null;
        #endregion

        #region Ctor
        public TabControlEx()
            : base()
        {
            // this is necessary so that we get the initial databound selected item
            this.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
            this.Loaded += TabControlEx_Loaded;
        }
        #endregion

        #region Public/Protected Methods
        /// <summary>
        /// get the ItemsHolder and generate any children
        /// </summary>
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            itemsHolder = GetTemplateChild("PART_ItemsHolder") as Panel;
            UpdateSelectedItem();
        }

        /// <summary>
        /// when the items change we remove any generated panel children and add any new ones as necessary
        /// </summary>
        /// <param name="e"></param>
        protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
        {
            base.OnItemsChanged(e);

            if (itemsHolder == null)
            {
                return;
            }

            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Reset:
                    itemsHolder.Children.Clear();
                    break;

                case NotifyCollectionChangedAction.Add:
                case NotifyCollectionChangedAction.Remove:
                    if (e.OldItems != null)
                    {
                        foreach (var item in e.OldItems)
                        {
                            ContentPresenter cp = FindChildContentPresenter(item);
                            if (cp != null)
                            {
                                itemsHolder.Children.Remove(cp);
                            }
                        }
                    }

                    // don't do anything with new items because we don't want to
                    // create visuals that aren't being shown

                    UpdateSelectedItem();
                    break;

                case NotifyCollectionChangedAction.Replace:
                    throw new NotImplementedException("Replace not implemented yet");
            }
        }

        /// <summary>
        /// update the visible child in the ItemsHolder
        /// </summary>
        /// <param name="e"></param>
        protected override void OnSelectionChanged(SelectionChangedEventArgs e)
        {
            base.OnSelectionChanged(e);
            UpdateSelectedItem();
        }

        /// <summary>
        /// copied from TabControl; wish it were protected in that class instead of private
        /// </summary>
        /// <returns></returns>
        protected TabItem GetSelectedTabItem()
        {
            object selectedItem = base.SelectedItem;
            if (selectedItem == null)
            {
                return null;
            }
            TabItem item = selectedItem as TabItem;
            if (item == null)
            {
                item = base.ItemContainerGenerator.ContainerFromIndex(base.SelectedIndex) as TabItem;
            }
            return item;
        }
        #endregion

        #region Private Methods

        /// <summary>
        /// in some scenarios we need to update when loaded in case the 
        /// ApplyTemplate happens before the databind.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void TabControlEx_Loaded(object sender, RoutedEventArgs e)
        {
            UpdateSelectedItem();
        }

        /// <summary>
        /// if containers are done, generate the selected item
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
        {
            if (this.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
            {
                this.ItemContainerGenerator.StatusChanged -= ItemContainerGenerator_StatusChanged;
                UpdateSelectedItem();
            }
        }

        /// <summary>
        /// generate a ContentPresenter for the selected item
        /// </summary>
        private void UpdateSelectedItem()
        {
            if (itemsHolder == null)
            {
                return;
            }

            // generate a ContentPresenter if necessary
            TabItem item = GetSelectedTabItem();
            if (item != null)
            {
                CreateChildContentPresenter(item);
            }

            // show the right child
            foreach (ContentPresenter child in itemsHolder.Children)
            {
                child.Visibility = ((child.Tag as TabItem).IsSelected) ? Visibility.Visible : Visibility.Collapsed;
            }
        }

        /// <summary>
        /// create the child ContentPresenter for the given item (could be data or a TabItem)
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        private ContentPresenter CreateChildContentPresenter(object item)
        {
            if (item == null)
            {
                return null;
            }

            ContentPresenter cp = FindChildContentPresenter(item);

            if (cp != null)
            {
                return cp;
            }

            // the actual child to be added.  cp.Tag is a reference to the TabItem
            cp = new ContentPresenter();
            cp.Content = (item is TabItem) ? (item as TabItem).Content : item;
            cp.ContentTemplate = this.SelectedContentTemplate;
            cp.ContentTemplateSelector = this.SelectedContentTemplateSelector;
            cp.ContentStringFormat = this.SelectedContentStringFormat;
            cp.Visibility = Visibility.Collapsed;
            cp.Tag = (item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item));
            itemsHolder.Children.Add(cp);
            return cp;
        }

        /// <summary>
        /// Find the CP for the given object.  data could be a TabItem or a piece of data
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        private ContentPresenter FindChildContentPresenter(object data)
        {
            if (data is TabItem)
            {
                data = (data as TabItem).Content;
            }

            if (data == null)
            {
                return null;
            }

            if (itemsHolder == null)
            {
                return null;
            }

            foreach (ContentPresenter cp in itemsHolder.Children)
            {
                if (cp.Content == data)
                {
                    return cp;
                }
            }

            return null;
        }
        #endregion
    }
}

Где бы вы шаблонировали его примерно так (вам может понадобиться развернуть его для левой / правой вкладки TabStripLocation)

<ControlTemplate x:Key="MainTabControlTemplateEx"
                TargetType="{x:Type controls:TabControlEx}">
    <Grid>

        <Grid.RowDefinitions>
            <RowDefinition x:Name="row0" Height="Auto"/>
            <RowDefinition x:Name="row1" Height="4"/>
            <RowDefinition x:Name="row2" Height="*"/>
        </Grid.RowDefinitions>

        <TabPanel x:Name="tabpanel" 
            Background="{StaticResource OutlookButtonHighlight}"
            Margin="0"
            Grid.Row="0"
            IsItemsHost="True" />

        <Grid x:Name="divider"
                Grid.Row="1" Background="Black" 
                HorizontalAlignment="Stretch"/>

        <Grid x:Name="PART_ItemsHolder"
                Grid.Row="2"/>
    </Grid>
    <!-- no content presenter -->
    <ControlTemplate.Triggers>
        <Trigger Property="TabStripPlacement" Value="Top">
            <Setter TargetName="tabpanel" Property="Grid.Row" Value="0"/>
            <Setter TargetName="divider" Property="Grid.Row" Value="1"/>
            <Setter TargetName="PART_ItemsHolder" Property="Grid.Row" Value="2" />
            <Setter TargetName="row0" Property="Height" Value="Auto" />
            <Setter TargetName="row1" Property="Height" Value="4" />
            <Setter TargetName="row2" Property="Height" Value="*" />
        </Trigger>
        <Trigger Property="TabStripPlacement" Value="Bottom">
            <Setter TargetName="tabpanel" Property="Grid.Row" Value="2" />
            <Setter TargetName="divider" Property="Grid.Row" Value="1" />
            <Setter TargetName="PART_ItemsHolder" Property="Grid.Row" Value="0" />
            <Setter TargetName="row0" Property="Height" Value="*" />
            <Setter TargetName="row1" Property="Height" Value="4" />
            <Setter TargetName="row2" Property="Height" Value="Auto" />
        </Trigger>
    </ControlTemplate.Triggers>

</ControlTemplate>

Который вы могли бы использовать вот так

<local:TabControlEx 
        IsSynchronizedWithCurrentItem="True" 
        ItemsSource="{Binding Path=Workspaces}" 
        Template="{StaticResource MainTabControlTemplateEx}">
</local:TabControlEx>

Он работает очень хорошо, и мы уже давно используем его для получения эффекта.

0 голосов
/ 01 апреля 2019

Не могу добавлять комментарии, но хочу поблагодарить sacha за ответ и немного его расширить. Возникла проблема с загрузкой этого TabControl, поэтому первая вкладка не отображалась (отсутствует в визуальном дереве). Добавление следующего кода в область «Public / Protected Methods» решает проблему.

    /// <summary>
    /// There was a flaky issue when first tab was uninitialized
    /// </summary>
    /// <param name="oldValue"></param>
    /// <param name="newValue"></param>
    protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
    {
        base.OnItemsSourceChanged(oldValue, newValue);
        UpdateSelectedItem();
    }
0 голосов
/ 03 июня 2012

Загружать содержимое вкладки динамически при выделении, чтобы интерфейс реагировал, используйте код, подобный приведенному ниже:

 private void tab_Selected(object sender, EventArgs e)
{
   //Get the selected tab
 Action loadTab = delegate
{
  LoadSelectedTab(tabItem);
}
Dispatcher.BeginInvoke(DispatcherPriority.Background, loadTab);
}
public void LoadSelectedTab(TabItem item)
{
  item.Content = new EmployeeTab();
  .....
}

Отклик пользовательского интерфейса будет очень быстрым, пользовательский интерфейс начнет загружаться очень быстро, и вы не увидите паузу для каких-либо задержек

0 голосов
/ 25 июля 2011

Проблема не в получении данных (их можно предварительно загрузить в отдельный поток), а в создании визуальных элементов в сетке данных.

Если вы проверяете с помощью Snoop, вы видите, что есть много визуальных элементов, если вам не нужны все функциональные возможности сетки данных, вы можете использовать более простое представление (ListView / ItemsControl / Custom)

...