无法访问WPF TabControl中的标签页视图模型

时间:2014-12-19 19:43:08

标签: wpf tabcontrol

我已经和这个问题争吵了一段时间,而且我找不到可行的答案,而且我确定这与我对WPF缺乏了解有很大关系。< / p>

我的程序的基本架构将类似于Visual Studio,其中的选项卡块可以以各种方式排列。为简单起见,我目前只有一个无法以任何方式排列的标签块 - 表单包含一个包含标签控件的自定义控件。

现在,每个标签页的内容将是一个自定义控件,它是文档的视图。标签页可能包含不同的文档,每个文档都有特定的自定义控件和文档视图模型配对。

我的主要问题是我无法弄清楚如何在标签页控件的代码隐藏中引用标签页的视图模型。

以下是TabPane的代码 - 作为选项卡块的自定义控件。它包括我从各种网站拼凑在一起的代码,用于向标签添加一个关闭按钮(稍后我可能会添加更多按钮)。

<UserControl x:Class="MyApp.TabPane"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:local="clr-namespace:MyApp"
         DataContext="{Binding RelativeSource={RelativeSource Self}}"
         mc:Ignorable="d" 
         d:DesignHeight="300" d:DesignWidth="300">
<DockPanel LastChildFill="True" >
    <TabControl ItemsSource="{Binding Path=ViewModel.TabPages}" SelectedItem="{Binding Path=ViewModel.ActivePage}" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch">
        <TabControl.Resources>
            <DataTemplate DataType="{x:Type local:NowPlayingViewModel}" >
                <local:NowPlayingControl />
            </DataTemplate>
            <DataTemplate DataType="{x:Type local:TypeEditorDocumentViewModel}" >
                <local:TypeEditorControl />
            </DataTemplate>
        </TabControl.Resources>
        <TabControl.ItemContainerStyle>
            <Style TargetType="TabItem">
                <Setter Property="HeaderTemplate" >
                    <Setter.Value>
                        <DataTemplate>
                            <StackPanel Orientation="Horizontal" Margin="0" Height="22">
                                <TextBlock VerticalAlignment="Center" Text="{Binding Path=Caption}" />
                                <local:LibraryTabHeaderButton Name="tabPageCloseButton" Width="20" Height="19" Margin="6,0,0,0" Padding="0" HorizontalAlignment="Center" VerticalAlignment="Center"
                                    Focusable="False" 
                                    Visibility="{Binding Path=AllowClose, Converter={local:BooleanToVisibilityConverter}}"
                                    Click="tabPageCloseButton_Click">
                                    <Path Data="M1,9 L9,1 M1,1 L9,9" Stroke="Black" StrokeThickness="2" />
                                </local:LibraryTabHeaderButton>
                            </StackPanel>
                        </DataTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </TabControl.ItemContainerStyle>
    </TabControl>
</DockPanel>

这是TabPane的代码隐藏。应用程序为TabPane分配了一个视图模型,以便视图模型可以提供一组预先存在的选项卡(将有一些选项卡只能存在于一个TabPane中且无法关闭)。关闭按钮将需要告诉视图模型哪个选项卡正在关闭。无论选项卡是否处于活动状态,任何可关闭的选项卡都可能会关闭。

    public partial class TabPane : UserControl
{
    private TabPaneViewModel viewModel = null;

    public TabPane()
    {
        viewModel = ApplicationManager.GetLibraryViewModel().GetNewTabPaneViewModel();

        InitializeComponent();
    }

    public TabPaneViewModel ViewModel
    {
        get { return viewModel; }
    }

    private void tabPageCloseButton_Click(object sender, EventArgs e)
    {
        //Button button = (Button)sender;
    }

TabPaneViewModel非常简单。它基本上包含一组标签页。

    public class TabPaneViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private readonly int id;
    private ObservableCollection<ILibraryDocumentViewModel> tabPages = new ObservableCollection<ILibraryDocumentViewModel>();
    private ILibraryDocumentViewModel activePage = null;

    public TabPaneViewModel(int id)
    {
        this.id = id;
        tabPages.CollectionChanged += tabPages_CollectionChanged;
    }

    private void tabPages_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if( !isDisposed && (e.Action == NotifyCollectionChangedAction.Add) )
            ActivePage = (ILibraryDocumentViewModel)e.NewItems[0];
    }

    public ObservableCollection<ILibraryDocumentViewModel> TabPages
    {
        get { return tabPages; }
    }

    public ILibraryDocumentViewModel ActivePage
    {
        get { return activePage; }
        set
        {
            activePage = value;
            OnNotifyPropertyChanged("ActivePage");
        }
    }

    private void OnNotifyPropertyChanged(string propertyName)
    {
        if( !isDisposed && (PropertyChanged != null) )
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

LibraryTabHeaderButton是一个带有click事件的简单自定义控件。我发现这个标题按钮不知道它与哪个标签相关联,所以我需要告诉它。我还没弄明白怎么做。

    public partial class LibraryTabHeaderButton : UserControl
{
    public event EventHandler Click;

    public LibraryTabHeaderButton()
    {
        InitializeComponent();
    }

    public int TabPageId { get; set; }

    private void OnClick(object sender, RoutedEventArgs args)
    {
        LibraryTabHeaderButtonClickEventArgs newArgs = new LibraryTabHeaderButtonClickEventArgs();

        newArgs.TabPageId = TabPageId;
        if( Click != null )
            Click(sender, newArgs);
    }
}

LibraryViewModel是文档的来源。它接收创建或打开文档的指令,然后将其添加到TabPane。

    public class LibraryViewModel
{
    private List<TabPaneViewModel> tabPanes = new List<TabPaneViewModel>();
    private static int nextTabPaneId = 1;
    private TabPaneViewModel activeTabPane = null;
    private NowPlayingViewModel nowPlayingViewModel = null;
    private static int nextTabPageId = 1;

    public TabPaneViewModel GetNewTabPaneViewModel()
    {
        TabPaneViewModel tabPane = new TabPaneViewModel(nextTabPaneId);

        nextTabPaneId++;
        tabPanes.Add(tabPane);

        if( activeTabPane == null )
            activeTabPane = tabPane;

        if( nowPlayingViewModel == null )
        {
            // Show the now playing tab on this pane
            nowPlayingViewModel = new NowPlayingViewModel(nextTabPageId);
            nextTabPageId++;
            tabPane.TabPages.Add(nowPlayingViewModel);
        }

        return tabPane;
    }

    public void DisplayDocument(LibraryDocumentType documentType)
    {
        if( activeTabPane != null )
        {
            ILibraryDocumentViewModel document = null;

            switch( documentType )
            {
                case LibraryDocumentType.TypeEditor:
                    document = new TypeEditorDocumentViewModel(nextTabPageId);
                    nextTabPageId++;
                    break;
            }

            activeTabPane.TabPages.Add(document);
        }
    }

}

这是ILibraryDocumentViewModel。

    public interface ILibraryDocumentViewModel
{
    int TabPageId { get; }
    LibraryDocumentType DocumentType { get; }
    string Caption { get; }
    bool AllowClose { get; }
    bool IsChanged { get; }
}

这是TypeEditorControl代码隐藏的一部分。我已经尝试了从这里访问视图模型的各种方法,但它始终为null。

    public partial class TypeEditorControl : UserControl
{
    private TypeEditorDocumentViewModel viewModel = null;

    public TypeEditorControl()
    {
        object v = this.ViewModel;
        viewModel = DataContext as TypeEditorDocumentViewModel;

        InitializeComponent();
        viewModel = DataContext as TypeEditorDocumentViewModel;

        UpdateEditMode();
    }

    public TypeEditorDocumentViewModel ViewModel
    {
        get { return viewModel; }
    }

这是TypeEditorControl xaml的一部分。

<UserControl x:Class="MyApp.TypeEditorControl"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         xmlns:local="clr-namespace:MyApp"
         mc:Ignorable="d" 
         d:DesignHeight="630">
<Grid Margin="0" Background="#FFF0F0F0">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <Label x:Name="categoryListLbl" Content="Type Categories" Grid.Column="0" HorizontalAlignment="Left" Margin="10,5,10,0" Grid.Row="0" VerticalAlignment="Top"/>
    <ListView x:Name="categoryListLvw" Margin="10,0" Grid.Row="1" Grid.ColumnSpan="2" ItemsSource="{Binding Path=ViewModel.TypeCategories}" SelectionChanged="categoryListLvw_SelectionChanged" SelectionMode="Single" >
        <ListView.View>
            <GridView>

我认为代码足够......现在,程序运行了,我可以调出各种文档,但文档不会显示视图模型中的数据。

因为我创建了文档视图模型并将其添加到TabPages ObservableCollection中,所以我无法在该文档控件的代码后面获得对文档视图模型的引用。当我添加TypeEditorViewModel时,TabPane会自动生成TypeEditorControl,但我似乎无法访问它们之间的链接。我目前的猜测是,我没有在TypeEditorControl xaml中正确设置DataContext(我没有尝试过DataContext和一个引用Self,没有工作,我也不知道还有什么可以尝试)。也许我的建筑不会支持我想要做的事情。也许我在问错误的问题。

我不知道下一步该尝试什么。感谢阅读这篇巨大帖子的任何人。

2 个答案:

答案 0 :(得分:1)

小而有效的样本:

enter image description here

代码隐藏:定义简单视图模型

namespace WpfApplication10
{
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();
            var appViewModel = new AppViewModel
            {
                HelloFromApp = "Hello from app VM!",
                Tab1ViewModel = new Tab1ViewModel {HelloFromTab1 = "Hello from tab 1 VM !"},
                Tab2ViewModel = new Tab2ViewModel {HelloFromTab2 = "Hello from tab 2 VM !"}
            };
            DataContext = appViewModel;
        }
    }

    internal abstract class ViewModel
    {
    }

    internal class AppViewModel : ViewModel
    {
        public string HelloFromApp { get; set; }
        public Tab1ViewModel Tab1ViewModel { get; set; }
        public Tab2ViewModel Tab2ViewModel { get; set; }
    }

    internal class Tab1ViewModel : ViewModel
    {
        public string HelloFromTab1 { get; set; }
    }

    internal class Tab2ViewModel : ViewModel
    {
        public string HelloFromTab2 { get; set; }
    }
}

XAML:在这里,我将向您展示如何在不同来源的Tab1中显示内容

<Window x:Class="WpfApplication10.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:wpfApplication10="clr-namespace:WpfApplication10"
        Title="MainWindow"
        Width="525"
        Height="350"
        d:DataContext="{d:DesignInstance wpfApplication10:AppViewModel}"
        mc:Ignorable="d">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="1*" />
        </Grid.RowDefinitions>
        <TextBlock Text="{Binding HelloFromApp}" />
        <TabControl Grid.Row="1">
            <TabItem Header="Tab1">
                <StackPanel>
                    <TextBlock Text="{Binding HelloFromApp, StringFormat='{}Some content from AppViewModel: {0}'}" />
                    <TextBlock Text="{Binding Tab1ViewModel.HelloFromTab1, StringFormat='{}Some content from AppViewModel.Tab1ViewModel: {0}'}" />
                    <TextBlock DataContext="{Binding Tab2ViewModel}" Text="{Binding HelloFromTab2, StringFormat='{}Some content from AppViewModel.Tab2ViewModel: {0}'}" />
                </StackPanel>
            </TabItem>
            <TabItem Header="Tab2" />
        </TabControl>
    </Grid>
</Window>

正如您所看到的,除了上一个示例之外,我从未设置DataContext以获得更简单的语法(将Text绑定与上面的绑定进行比较)。人们在MS上做的很好,默认情况下DataContext继承父值,正如你所看到的那样真的很有帮助。

对于停靠标签:https://avalondock.codeplex.com/

答案 1 :(得分:1)

我碰巧在我的TypeEditorControl的覆盖中探索并找到了OnApplyTemplate方法。我读了一下它并尝试了它,发现在这个覆盖中,DataContext被设置为我想要的对象。这让我有机会获得它的参考。以下是TypeEditorControl的更新代码。

    public partial class TypeEditorControl : UserControl
{
    private TypeEditorDocumentViewModel viewModel = null;

    public TypeEditorControl()
    {
        InitializeComponent();
    }

    public override void OnApplyTemplate()
    {
        viewModel = DataContext as TypeEditorDocumentViewModel;

        base.OnApplyTemplate();

        UpdateEditMode();
    }

    public TypeEditorDocumentViewModel ViewModel
    {
        get { return viewModel; }
    }

然后我想知道标签页标题是否也是如此。果然,它也在那里工作。这是LibraryTabHeaderButton的新代码。我开始交替使用标签页ID和文档ID,所以我为此道歉。

    public partial class LibraryTabHeaderButton : UserControl
{
    public event LibraryTabHeaderButtonClickEventHandler Click;

    private int documentId = 0;

    public LibraryTabHeaderButton()
    {
        InitializeComponent();
    }

    public override void OnApplyTemplate()
    {
        ILibraryDocumentViewModel viewModel = this.DataContext as ILibraryDocumentViewModel;

        if( viewModel != null )
            documentId = viewModel.DocumentId;

        base.OnApplyTemplate();
    }

    private void button_Click(object sender, RoutedEventArgs args)
    {
        LibraryTabHeaderButtonClickEventArgs newArgs = new LibraryTabHeaderButtonClickEventArgs();

        newArgs.TabPageId = documentId;
        if( Click != null )
            Click(sender, newArgs);
    }
}

更新TabPane标头关闭事件以将请求发送到TabPaneViewModel。

    public partial class TabPane : UserControl
{
    ...

    private void tabPageCloseButton_Click(object sender, LibraryTabHeaderButtonClickEventArgs args)
    {
        viewModel.CloseTabPage(args.TabPageId);
    }
}

    public class TabPaneViewModel : INotifyPropertyChanged
{
    private ObservableCollection<ILibraryDocumentViewModel> tabPages = new ObservableCollection<ILibraryDocumentViewModel>();

    ...

    public void CloseTabPage(int documentId)
    {
        ILibraryDocumentViewModel document = tabPages.First(
            entry => entry.DocumentId == documentId);

        if( document != null )
            tabPages.Remove(document);
    }

    ...
}

我确定这不是最好的解决方案,但现在我可以在标签页的代码后面访问视图模型,我可以关闭非活动标签页。