2

ワークスペース(Josh Smithによるブログからの参照)と呼んでいるものを表すユーザーコントロールを作成しようとしています。ワークスペースはタブコントロールに表示されます。タブ付きのインターフェイスを使用して、excalワークブックのブラウザのように開いたさまざまなドキュメントを管理することを目指しています。

ユーザーが新しいワークスペースを開くたびに、そのワークスペースがタブコントロールに表示されます。各ワークスペースはユーザーコントロールの形式を取り、各ワークスペースには独自のビューモデルがあります。タブヘッダーにビューモデルのプロパティを表示したいのですが、ユーザーコントロールを介してプロパティとして公開する必要があると思います。

これまでのところ、多くの問題に遭遇するまで私が最も気に入った最もクリーンなソリューションは、データテンプレートを使用することでした。基本的に私は次のことをしました:

<DataTemplate x:Key="WorkspaceItem">
            <DockPanel Width="120">
                <ContentPresenter 
                    Content="{Binding Title}" 
                    VerticalAlignment="Center" 
                    />
            </DockPanel>
        </DataTemplate>     

<DataTemplate DataType="{x:Type CustomerViewModel}">
   <workspace:CustomerWorkspace />
</DataTemplate>

<TabControl ItemsSource="{Binding Workspaces}"
            ItemTemplate="{StaticResource WorkspaceItem}"/>

TabControl.ItemsSourceは、すべてのワークスペースを含む(オブジェクトの)observablecollectionにバインドされています。

これは、2つのことを除いてうまく機能します。

  1. 複数の顧客を開くと、複数のワークスペースが開きます。DataTemplateのリサイクルのため、あるタブから別のタブに切り替えると状態が失われます。したがって、バインドされていないものはすべて状態を失います。

  2. 異なるワークスペース(異なるデータテンプレートを使用する)間のスワッピングのパフォーマンスは非常に遅くなります。

だから...私はSOの別のユーザーから、ユーザーコントロールをObservableCOllectionに追加し、データテンプレートを破棄するという提案を見つけました。これで、状態を失う問題の1つが解決されます。しかし、今私は2つの残りの問題に直面しています。

  1. DataTemplateを使用せずにTabItem.Headerプロパティを設定するにはどうすればよいですか
  2. タブが同じDataTemplateでない限り、タブ間を行ったり来たりする速度はまだ遅いです。

次に、コードビハインドのObservableCollectionにTabItemを実際に追加し、TabItem.Contentプロパティをユーザーコントロールのプロパティに設定します。DataTemplatesの使用を削除したため、速度の問題は、状態の喪失の問題と同様に解消されました。ただし、TabItem.headerを、タブヘッダーに表示される必要があるユーザーコントロールのCustome「Title」プロパティにバインドする問題に悩まされています。

したがって、この非常に長い投稿の後、私の質問は次のとおりです。

  1. データテンプレートを使用して、リサイクルと状態の損失を防ぐために、コレクション内のアイテムごとに新しいインスタンスを作成するように強制する方法はありますか?

    1a。上記の投稿で述べたものよりも優れた代替手段はありますか?

  2. タブアイテムのバックエンドコード構築ではなく、Xamlを介してこれらすべてを実行する方法はありますか?

4

1 に答える 1

8

The default behavior of WPF is to unload items which are not visible, which includes unloading TabItems which are not visible. This means when you go back to the tab, the TabItem gets re-loaded, and anything not bound (such as a scroll position, control states, etc) will get reset.

There was a good site here which contains code to extend the TabControl and stop it from destroying its TabItems when switching tabs, however it no longer seems to exist now.

Here's a copy of the code, although I've made some changes to it. It preserves the ContentPresenter of TabItems when switching tabs, and uses it to redraw the TabItem when you go back to the page. It takes up a bit more memory, however I find it better on performance since the TabItem no longer has to re-create all the controls that were on it.

// Extended TabControl which saves the displayed item so you don't get the performance hit of 
// unloading and reloading the VisualTree when switching tabs

// Obtained from http://eric.burke.name/dotnetmania/2009/04/26/22.09.28
// and made a some modifications so it reuses a TabItem's ContentPresenter when doing drag/drop operations

[TemplatePart(Name = "PART_ItemsHolder", Type = typeof(Panel))]
public class TabControlEx : System.Windows.Controls.TabControl
{
    // Holds all items, but only marks the current tab's item as visible
    private Panel _itemsHolder = null;

    // Temporaily holds deleted item in case this was a drag/drop operation
    private object _deletedObject = null;

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

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

    /// <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();

                if (base.Items.Count > 0)
                {
                    base.SelectedItem = base.Items[0];
                    UpdateSelectedItem();
                }

                break;

            case NotifyCollectionChangedAction.Add:
            case NotifyCollectionChangedAction.Remove:

                // Search for recently deleted items caused by a Drag/Drop operation
                if (e.NewItems != null && _deletedObject != null)
                {
                    foreach (var item in e.NewItems)
                    {
                        if (_deletedObject == item)
                        {
                            // If the new item is the same as the recently deleted one (i.e. a drag/drop event)
                            // then cancel the deletion and reuse the ContentPresenter so it doesn't have to be 
                            // redrawn. We do need to link the presenter to the new item though (using the Tag)
                            ContentPresenter cp = FindChildContentPresenter(_deletedObject);
                            if (cp != null)
                            {
                                int index = _itemsHolder.Children.IndexOf(cp);

                                (_itemsHolder.Children[index] as ContentPresenter).Tag =
                                    (item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item));
                            }
                            _deletedObject = null;
                        }
                    }
                }

                if (e.OldItems != null)
                {
                    foreach (var item in e.OldItems)
                    {

                        _deletedObject = item;

                        // We want to run this at a slightly later priority in case this
                        // is a drag/drop operation so that we can reuse the template
                        this.Dispatcher.BeginInvoke(DispatcherPriority.DataBind,
                            new Action(delegate()
                        {
                            if (_deletedObject != null)
                            {
                                ContentPresenter cp = FindChildContentPresenter(_deletedObject);
                                if (cp != null)
                                {
                                    this._itemsHolder.Children.Remove(cp);
                                }
                            }
                        }
                        ));
                    }
                }

                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>
    /// generate a ContentPresenter for the selected item
    /// </summary>
    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>
    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>
    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;
    }

    /// <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;
        }

        if (_deletedObject == selectedItem)
        { 

        }

        TabItem item = selectedItem as TabItem;
        if (item == null)
        {
            item = base.ItemContainerGenerator.ContainerFromIndex(base.SelectedIndex) as TabItem;
        }
        return item;
    }
}

The TabControl template I usually use looks something like this:

<Style x:Key="TabControlEx_NoHeadersStyle" TargetType="{x:Type local:TabControlEx}">
    <Setter Property="SnapsToDevicePixels" Value="true"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type localControls:TabControlEx}">
                <DockPanel>
                    <!-- This is needed to draw TabControls with Bound items -->
                    <StackPanel IsItemsHost="True" Height="0" Width="0" />
                    <Grid x:Name="PART_ItemsHolder" />
                </DockPanel>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

You can also simplify your XAML by using an implicit DataTemplate instead of an ItemTemplate since your ViewModel will be placed in your TabItem.Content. I'm also not too sure what you're asking about the header, but if I understand you correctly you can just set the header in another implicit style for the TabItem

<Window.Resources>
    <DataTemplate DataType="{x:Type CustomerViewModel}">
       <workspace:CustomerWorkspace />
    </DataTemplate>
</Window.Resources>

<TabControl ItemsSource="{Binding Workspaces}">
    <TabControl.Resources>
        <Style TargetType="{x:Type TabItem}">
            <Setter Property="Header" Value="{Binding HeaderProperty}" />
        </Style>
    </TabControl.Resources>
</TabControl>
于 2012-09-14T21:19:19.817 に答える