WPF 自定义 MultiSelector 不会使用 ItemTemplateSelector(但会使用 ItemTemplate)

WPF custom MultiSelector won't use ItemTemplateSelector (but will use ItemTemplate)

我搜索并发现了很多与我类似的问题,但其中 none 似乎很符合我的情况。不可否认,我的情况有点极端。我希望有人能发现我在这里遗漏的东西。

我一直在使用派生自 MultiSelector 的自定义 ItemsControl。我有一个自定义 DataTemplate 来绘制其中的项目。如果 且仅当 我在控件上使用 ItemTemplate 属性 时,它们的绘制就很好。

但是当我尝试使用 ItemTemplateSelector 属性 时,我对 SelectTemplate 的覆盖没有被调用。我已验证它已创建,然后设置为控件的 ItemTemplateSelector。但是它的 SelectTemplate 覆盖的断点永远不会被击中。

最终效果是以前由我的唯一一个 DataTemplate 精美绘制的漂亮形状现在只显示为字符串名称。

我使用的视图是这样的:

<UserControl x:Class="MyApp.Shapes.ShapeCanvas"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:gcs="clr-namespace:MyApp.Shapes"
             xmlns:gcp="clr-namespace:MyApp.Properties"
             xmlns:net="http://schemas.mycompany.com/mobile/net"
             >

    <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="/MyApp.Core;component/Resources/Styles/ShapeItemStyle.xaml" />
            </ResourceDictionary.MergedDictionaries>

            <!-- 
            Default data template for most ShapeVm types, custom data type for PolyLineVm 
            I've removed the contents for brevity but they draw Paths objects normally
            -->

            <DataTemplate x:Key="ShapeTemplate" DataType="{x:Type gcs:ShapeVm}"/>
            <DataTemplate x:Key="PolylineTemplate" DataType="{x:Type gcs:PolyLineVm}"/>

            <!-- ShapeTemplateSelector to pick the right one -->
            <gcs:ShapeTemplateSelector x:Key="ShapeTemplateSelector"
                                       DefaultTemplate="{StaticResource ShapeTemplate}"
                                       PolyLineTemplate="{StaticResource PolylineTemplate}"/>
        </ResourceDictionary>
    </UserControl.Resources>

    <Canvas x:Name="Scene">
        <gcs:ShapesControl x:Name="ShapesControl"
                           HorizontalAlignment="Stretch"
                           VerticalAlignment="Stretch"
                           ItemContainerStyle="{StaticResource ShapeItemStyle}"
                           ItemsSource="{Binding Shapes}"
                           ItemTemplateSelector="{StaticResource ShapeTemplateSelector}"
        >
            <!-- If I use this line instead of ItemContainerStyle, It *does* pick up shape template -->
            <!-- ItemTemplate="{StaticResource ShapeTemplate}" -->

            <gcs:ShapesControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas Background="Transparent" IsItemsHost="True" />
                </ItemsPanelTemplate>
            </gcs:ShapesControl.ItemsPanel>

        </gcs:ShapesControl>
    </Canvas>
</UserControl>

自定义数据模板选择器

public class ShapeTemplateSelector : DataTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        *** THIS NEVER EVEN GETS CALLED ***
        return item is PolyLineVm ? PolyLineTemplate : DefaultTemplate;
    }

    public DataTemplate PolyLineTemplate { get; set; }
    public DataTemplate DefaultTemplate { get; set; }
}

自定义多选器("ShapesControl")

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

namespace MyApp.Shapes
{
    // Created By: 
    // Date: 2017-08-25

    using System.Linq;
    using System.Windows;
    using System.Windows.Controls.Primitives;
    using System.Windows.Input;


    /// <summary>
    /// ShapesControl - our own version of a WPF MultiSelector.  Basically an
    /// ItemsControl that can select multiple items at once.  We need this to
    /// handle user manipulation of shapes on the ShapeCanvas and, potentially,
    /// elsewhere.
    /// </summary>
    public class ShapesControl : MultiSelector
    {
        protected override bool IsItemItsOwnContainerOverride(object item)
        {
            return (item is ShapeItem);
        }

        protected override DependencyObject GetContainerForItemOverride()
        {
            // Each item we display is wrapped in our own container: ShapeItem
            // This override is how we enable that.

            return new ShapeItem();
        }

        // ...Other handlers are multi-selection overrides and removed for clarity
    }
}

最后,我用来绘制自定义 ShapeItem 的 ShapeItemStyle

<Style x:Key="ShapeItemStyle"
       TargetType="{x:Type gcs:ShapeItem}"
       d:DataContext="{d:DesignInstance {x:Type gcs:ShapeVm}}"
           >
  <Setter Property="SnapsToDevicePixels" Value="true" />
  <Setter Property="Background" Value="Transparent"/>
  <Setter Property="Canvas.Left" Value="{Binding Path=Left, Mode=OneWay}"/>
  <Setter Property="Canvas.Top" Value="{Binding Path=Top, Mode=OneWay}"/>


  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate  TargetType="{x:Type gcs:ShapeItem}">
        <Grid>
          >
          <!-- ContentPresenter for the ShapeVm that this ShapeItem contains -->

          <ContentPresenter x:Name="PART_Shape"
                            Content="{TemplateBinding ContentControl.Content}"
                            ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}"
                            ContentStringFormat="{TemplateBinding ContentControl.ContentStringFormat}"
                            HorizontalAlignment="{TemplateBinding Control.HorizontalContentAlignment}"
                            VerticalAlignment="{TemplateBinding Control.VerticalContentAlignment}"
                            IsHitTestVisible="False"
                            SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}"
                            RenderTransformOrigin="{TemplateBinding ContentControl.RenderTransformOrigin}"/>
        </Grid>

      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

(编辑以根据请求添加 ShapeItem。请注意,这包括与上面的自定义 MultiSelector ("ShapesControl") 交互的选择代码。为了简洁起见,我从 ShapesControl 代码中删除了其中一些函数,因为它们被触发了通过鼠标点击,我看不出他们怎么可能阻止自定义 DataTemplateSelector 被调用。但我已经在此处发布了所有 ShapeItem)

namespace MyApp.Shapes
{
    [TemplatePart(Name="PART_Shape", Type=typeof(ContentPresenter))]
    public class ShapeItem : ContentControl
    {
        public ShapeVm ShapeVm => DataContext as ShapeVm;
        public ShapeType ShapeType => ShapeVm?.ShapeType ?? ShapeType.None;
        static ShapeItem()
        {
            DefaultStyleKeyProperty.OverrideMetadata
                (typeof(ShapeItem), 
                 new FrameworkPropertyMetadata(typeof(ShapeItem)));
        }
        private bool WasSelectedWhenManipulationStarted { get; set; }

        protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            base.OnMouseLeftButtonDown(e);
            ParentSelector?.NotifyItemClicked(this, true);
            e.Handled = true;

        }

        // The following ShapeItem manipulation handlers only handle the case of
        // moving the shape as a whole across the canvas.  These handlers do NOT
        // handle the case of resizing the shape.  Those handlers are on the
        // ShapeHandleThumb class.

        protected override void OnManipulationStarting(ManipulationStartingEventArgs e)
        {
            // The Canvas defines the coordinates for manipulation

            e.ManipulationContainer = this.FindVisualParent<Canvas>();
            base.OnManipulationStarting(e);
            e.Handled = true;
        }
        protected override void OnManipulationStarted(ManipulationStartedEventArgs e)
        {
            Debug.Assert(e.ManipulationContainer is Canvas);
            base.OnManipulationStarted(e);

            // If this item was selected already, this manipulation might be a 
            // move.  In that case, wait until we're done with the manipulation
            // before deciding whether to notify the control.

            if (IsSelected)
                WasSelectedWhenManipulationStarted = true;
            else
                ParentSelector.NotifyItemClicked(this, false);

            e.Handled = true;
        }
        protected override void OnManipulationDelta(ManipulationDeltaEventArgs e)
        {
            Debug.Assert(e.ManipulationContainer is Canvas);
            base.OnManipulationDelta(e);
            ParentSelector.NotifyItemMoved(e.DeltaManipulation.Translation);
            e.Handled = true;
        }

        protected override void OnManipulationCompleted(ManipulationCompletedEventArgs e)
        {
            Debug.Assert(e.ManipulationContainer is Canvas);
            base.OnManipulationCompleted(e);
            if (WasSelectedWhenManipulationStarted)
            {
                // If nothing moved appreciably, unselect everything.  This is how I
                // am detecting just a Tap.  I have to think there is a better way...

                var t = e.TotalManipulation.Translation;
                if (Math.Abs(t.X) < 0.0001 && Math.Abs(t.Y) < 0.0001)
                    ParentSelector.NotifyItemClicked(this, false);

            }
            e.Handled = true;
        }

        private bool IsControlKeyPressed => 
            (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control;

        internal ShapesControl ParentSelector =>
            ItemsControl.ItemsControlFromItemContainer(this) as ShapesControl;

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            Debug.Assert(ReferenceEquals(
                ParentSelector.ItemContainerGenerator.ItemFromContainer(this), 
                ShapeVm));
        }

        public static readonly DependencyProperty IsSelectedProperty = 
            Selector.IsSelectedProperty.AddOwner(
                typeof(ShapeItem), 
                new FrameworkPropertyMetadata(false, OnIsSelectedChanged));

        public bool IsSelected
        {
            get
            {
                var value = GetValue(IsSelectedProperty);
                return value != null && (bool)value;
            }
            set { SetValue(IsSelectedProperty, value); }
        }
        private static void OnIsSelectedChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
        {
            if (!(target is ShapeItem item))
                return;

            var evt = (bool)e.NewValue ? Selector.SelectedEvent : Selector.UnselectedEvent;
            item.RaiseEvent(new RoutedEventArgs(evt, item));
        }
    }
}

问题在于以下代码:

protected override DependencyObject GetContainerForItemOverride()
{
    // Each item we display is wrapped in our own container: ShapeItem
    // This override is how we enable that.

    return new ShapeItem();
}

当您覆盖 GetContainerForItemOverride 方法时,您的代码有责任使用 ItemTemplateSelector 并将其附加到 ItemsControl。