TabControl 运行时的 DataTemplate 问题

DataTemplate problems at runtime with TabControl

我正在尝试使用 view-model-first 方法并为我的自定义图表控件创建了一个 view-model。现在,在我的表单中,我想要一个 TabControl 来显示定义如下的 XAML-defined 图表列表:

<coll:ArrayList x:Key="ChartListTabs" x:Name="ChartList">
    <VM:MyChartViewModel x:Name="ChartVM_Today" ChartType="Today" ShortName="Today"/>
    <VM:MyChartViewModel x:Name="ChartVM_Week" ChartType="Week" ShortName="This Week"/>
    <VM:MyChartViewModel x:Name="ChartVM_Month" ChartType="Month" ShortName="This Month"/>
    <VM:MyChartViewModel x:Name="ChartVM_Qtr" ChartType="Quarter" ShortName="This Quarter"/>
    <VM:MyChartViewModel x:Name="ChartVM_Year" ChartType="Year" ShortName="This Year"/>
    <VM:MyChartViewModel x:Name="ChartVM_Cust" ChartType="Custom" ShortName="Custom"/>
</coll:ArrayList>

正在尝试为我的选项卡 header 和内容指定数据模板,我有这个:

<DataTemplate x:Key="tab_header">
    <TextBlock Text="{Binding ShortName}" FontSize="16" />
</DataTemplate>
<DataTemplate x:Key="tab_content" DataType="{x:Type VM:MyChartViewModel}" >
    <local:MyChartControl/>
</DataTemplate>

我的TabControl是这样的:

<TabControl ItemsSource="{StaticResource ChartListTabs}"
            ItemTemplate="{StaticResource tab_header}"
            ContentTemplate="{StaticResource tab_content}" 
            IsSynchronizedWithCurrentItem="True">
    <!-- nothing here :) -->
</TabControl>

发生的事情是设计器正确显示了选项卡并且第一个选项卡内容(无法切换选项卡,因为它们是动态创建的)显然显示了第一个图表的正确视图,但是当我 运行在应用程序中,所有选项卡都显示相同的、默认的、未初始化的内容(即没有设置任何属性的相同图表控件)。此外,实例似乎是相同的,即更改我的自定义控件上的某些内容(例如日期框),这会显示在所有选项卡上。

在我看来 TabControl 内容中的控件(视图)保持不变(TabControl 这样做,正如我在其他地方读到的那样)并且应该只更改 DataContext当标签更改时,但显然没有。


备注:

这是我的解决方案,里面使用了您的代码,请尝试检查一下。

Xaml

<Window x:Class="TabControTemplatingHelpAttempt.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:collections="clr-namespace:System.Collections;assembly=mscorlib"
    xmlns:tabControTemplatingHelpAttempt="clr-namespace:TabControTemplatingHelpAttempt"
    Title="MainWindow" Height="350" Width="525">
<Window.Resources>
    <collections:ArrayList x:Key="ChartListTabs" x:Name="ChartList">
        <tabControTemplatingHelpAttempt:MyChartViewModel x:Name="ChartVM_Today" ChartType="Today"   ShortName="Today"/>
        <tabControTemplatingHelpAttempt:MyChartViewModel x:Name="ChartVM_Week" ChartType= "Week"    ShortName="This Week"/>
        <tabControTemplatingHelpAttempt:MyChartViewModel x:Name="ChartVM_Month" ChartType="Month"   ShortName="This Month"/>
        <tabControTemplatingHelpAttempt:MyChartViewModel x:Name="ChartVM_Qtr" ChartType=  "Quarter" ShortName="This Quarter"/>
        <tabControTemplatingHelpAttempt:MyChartViewModel x:Name="ChartVM_Year" ChartType= "Year"    ShortName="This Year"/>
        <tabControTemplatingHelpAttempt:MyChartViewModel x:Name="ChartVM_Cust" ChartType= "Custom"  ShortName="Custom"/>
    </collections:ArrayList>
    <DataTemplate x:Key="TabHeader" DataType="{x:Type tabControTemplatingHelpAttempt:MyChartViewModel}">
        <TextBlock Text="{Binding ShortName}" FontSize="16" />
    </DataTemplate>
    <DataTemplate x:Key="TabContent" DataType="{x:Type tabControTemplatingHelpAttempt:MyChartViewModel}" >
        <tabControTemplatingHelpAttempt:MyChartControl Tag="{Binding ChartType}"/>
    </DataTemplate>
</Window.Resources>
<Grid>
    <TabControl ItemsSource="{StaticResource ChartListTabs}"
        ItemTemplate="{StaticResource TabHeader}"
        ContentTemplate="{StaticResource TabContent}" 
        IsSynchronizedWithCurrentItem="True"/>
</Grid></Window>

转换器代码

    public class ChartType2BrushConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var key = (ChartType) value;
        SolidColorBrush brush;
        switch (key)
        {
            case ChartType.Today:
                brush = Brushes.Tomato;
                break;
            case ChartType.Week:
                brush = Brushes.GreenYellow;
                break;
            case ChartType.Month:
                brush = Brushes.Firebrick;
                break;
            case ChartType.Quarter:
                brush = Brushes.Goldenrod;
                break;
            case ChartType.Year:
                brush = Brushes.Teal;
                break;
            case ChartType.Custom:
                brush = Brushes.Blue;
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }
        return brush;
    }

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

主虚拟机

public class MyChartViewModel:BaseObservableDependencyObject
{
    private ChartType _chartType;
    private string _shortName;

    public ChartType ChartType
    {
        get { return _chartType; }
        set
        {
            _chartType = value;
            OnPropertyChanged();
        }
    }

    public string ShortName
    {
        get { return _shortName; }
        set
        {
            _shortName = value;
            OnPropertyChanged();
        }
    }
}

public enum ChartType
{
    Today,
    Week,  
    Month,  
    Quarter,
    Year,  
    Custom,
}

内部用户控制XAML

<UserControl x:Class="TabControTemplatingHelpAttempt.MyChartControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:tabControTemplatingHelpAttempt="clr-namespace:TabControTemplatingHelpAttempt">
<UserControl.Resources>
    <tabControTemplatingHelpAttempt:ChartType2BrushConverter x:Key="ChartType2BrushConverterKey" />
    <DataTemplate x:Key="UserContentTemplateKey" DataType="{x:Type tabControTemplatingHelpAttempt:MyChartViewModel}">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition></RowDefinition>
                <RowDefinition></RowDefinition>
            </Grid.RowDefinitions>
            <Rectangle Grid.Row="0" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
                       Fill="{Binding ChartType, Converter={StaticResource ChartType2BrushConverterKey}}"/>
            <TextBlock Grid.Row="0" Text="{Binding ShortName, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" VerticalAlignment="Center" HorizontalAlignment="Center"/>
            <Grid Grid.Row="1" Tag="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}, Path=DataContext.ChartType, UpdateSourceTrigger=PropertyChanged}">
                <Grid.DataContext>
                    <tabControTemplatingHelpAttempt:TabContentDataContext/>
                </Grid.DataContext>
                <Rectangle VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
                                   Fill="{Binding BackgroundBrush}"/>
                <TextBlock Text="{Binding Description, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}" VerticalAlignment="Center" HorizontalAlignment="Center"/>
            </Grid>
        </Grid>
    </DataTemplate>
</UserControl.Resources>
<Grid>
    <ContentControl Content="{Binding }" ContentTemplate="{StaticResource UserContentTemplateKey}"/>
    <!--<Grid.DataContext>
        <tabControTemplatingHelpAttempt:TabContentDataContext/>
    </Grid.DataContext>
    <Rectangle VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
                       Fill="{Binding BackgroundBrush}"/>
    <TextBlock Text="{Binding Code, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" VerticalAlignment="Center" HorizontalAlignment="Center"/>-->
</Grid>

请注意,如果您注释掉 Grid.DataContext 标记并在 ContentControl 标记中进行注释,您的内部内容将不会更新,因为它不是根据交付的 MyChartViewModel 创建的。别处 我看不出你的代码有任何问题。

内部用户控制VM

public class TabContentDataContext:BaseObservableObject
{
    private string _code;
    private Brush _backgroundBrush;


    public TabContentDataContext()
    {
        Init();
    }

    private void Init()
    {
        var code = GetCode();
        Code = code.ToString();
        BackgroundBrush = code%2 == 0 ? Brushes.Red : Brushes.Blue;
    }

    public virtual int GetCode()
    {
        return GetHashCode();
    }

    public string Code
    {
        get { return _code; }
        set
        {
            _code = value;
            OnPropertyChanged();
        }
    }


    public Brush BackgroundBrush
    {
        get { return _backgroundBrush; }
        set
        {
            _backgroundBrush = value;
            OnPropertyChanged();
        }
    }
}

可观察对象代码

    /// <summary>
/// implements the INotifyPropertyChanged (.net 4.5)
/// </summary>
public class BaseObservableObject : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        var handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }

    protected virtual void OnPropertyChanged<T>(Expression<Func<T>> raiser)
    {
        var propName = ((MemberExpression)raiser.Body).Member.Name;
        OnPropertyChanged(propName);
    }

    protected bool Set<T>(ref T field, T value, [CallerMemberName] string name = null)
    {
        if (!EqualityComparer<T>.Default.Equals(field, value))
        {
            field = value;
            OnPropertyChanged(name);
            return true;
        }
        return false;
    }
}

更新

基本可观察依赖对象代码

    /// <summary>
///  dependency object that implements the INotifyPropertyChanged (.net 4.5)
/// </summary>
public class BaseObservableDependencyObject : DependencyObject, INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        var handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }

    protected virtual void OnPropertyChanged<T>(Expression<Func<T>> raiser)
    {
        var propName = ((MemberExpression)raiser.Body).Member.Name;
        OnPropertyChanged(propName);
    }

    protected bool Set<T>(ref T field, T value, [CallerMemberName] string name = null)
    {
        if (!EqualityComparer<T>.Default.Equals(field, value))
        {
            field = value;
            OnPropertyChanged(name);
            return true;
        }
        return false;
    }
}

此致。

在测试 Ilan 的回答时,我发现当在控件内声明 DataContext 时(即通过 UserControl.DataContext 标记作为某些 class 的实例),它会强加一个控件上的特定对象实例以及将某些其他对象数据绑定到它的能力丢失(可能是因为 WPF 运行-time 使用 SetData 而不是 SetCurrentData)。

在设计器中 "test" 控件的推荐方法是 d:DataContext 声明(仅适用于设计器)。