【问题标题】:Unable to access tab page view model in WPF TabControl无法访问 WPF TabControl 中的标签页视图模型
【发布时间】:2014-12-23 01:53:03
【问题描述】:

我为这个问题纠结了一段时间,也没有找到可行的答案,相信这与我对 WPF 缺乏了解有很大关系。

我的程序的基本架构将类似于 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 是一个带有点击事件的简单自定义控件。我发现这个标题按钮不知道它与哪个选项卡相关联,所以我需要以某种方式告诉它。我还没想好怎么做。

    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 代码隐藏的一部分。我尝试了各种方法从这里访问视图模型,但它始终为空。

    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,但都没有工作,我不知道还能尝试什么)。也许我的架构不支持我正在尝试做的事情。也许我问错了问题。

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

【问题讨论】:

    标签: wpf tabcontrol


    【解决方案1】:

    一个小而有效的样本:

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

    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/

    【讨论】:

    • 虽然您的回答对我来说非常有用,但我无法弄清楚如何将它应用到我正在做的事情中。
    • 你自己修好了!
    【解决方案2】:

    我碰巧在我的 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);
        }
    
        ...
    }
    

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-10-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-10-25
      • 1970-01-01
      相关资源
      最近更新 更多