【问题标题】:WPF: Binding TreeView in MVVM way step by step tutorialWPF:以 MVVM 方式绑定 TreeView 分步教程
【发布时间】:2012-12-09 17:02:52
【问题描述】:

请看下一篇文章。这个原来的一题内容已经被删除了,因为没有任何意义。简而言之,我询问了如何以 MVVM 方式使用 XmlDataProvider 将 XML(我在解析 DLL 程序集时错误生成)绑定到 TreeView。但后来我明白这种方法是错误的,于是我切换到数据实体模型的生成(只写代表我想在树中公开的所有实体的类)而不是 XML。

所以,下一篇文章中的结果。目前我不时更新这个“文章”,所以 F5,和

享受阅读!

【问题讨论】:

    标签: wpf xaml mvvm binding treeview


    【解决方案1】:

    简介

    我发现阅读this文章的正确方法

    这是一个很长的故事,你们大多数人可以跳过它:)但是那些想要了解问题和解决方案的人必须阅读所有内容!

    我是 QA,前段时间负责我点击的产品的自动化。幸运的是,这个自动机不是发生在一些测试工具中,而是发生在 Visual Studio 中,因此它最大程度地接近开发。

    对于我们的自动化,我们使用由 MbUnit(Gallio 作为运行器)和 MINT(MbUnit 的补充,由我们合作的客户编写)组成的框架。 MbUnit 为我们提供了测试装置和测试,而 MINT 添加了额外的更小的层——测试中的操作。例子。夹具称为“FilteringFixture”。它由大量的测试组成,例如“TestingFilteringById”或“TestingFilteringWithSpecialChars”等。每个测试都包含动作,它们是我们测试的原子单元。操作示例 - “打开应用程序(参数)”、“OpenFilterDialog”等。

    我们已经有很多测试,其中包含很多动作,一团糟。他们使用我们 QA 产品的内部 API。此外,我们开始研究一种新的自动化方法——通过 Microsoft UI 自动化实现 UI 自动化(抱歉重言式)。因此,对于管理者来说,一些“导出器”或“报告器”工具的必要性变得非常严重。

    前段时间我有一个任务要开发一些应用程序,它可以解析一个 DLL(它包含所有的装置、测试和动作),并以人类可读的格式(TXT、HTML、CSV、XML)导出它的结构, 任何其他)。但是,在那之后,我去度假(2周)。

    碰巧,我的女朋友一直到她家去度假(她也得到了它),而我一个人呆在家里。一直在思考我要做什么(2 周),我记得那个“编写导出器工具任务”以及我计划开始学习 WPF 的时间。所以,我决定在假期里完成我的任务,并为 WPF 编写一个应用程序。那个时候听说了一些关于 MVVM 的事情,就决定用纯 MVVM 来实现。

    可以用fixrtures等解析DLL的DLL编写得相当快(~1-2天)。之后我开始使用 WPF,本文将向您展示它是如何结束的。

    我度过了假期的大部分时间(将近 8 天!),试图在我的头脑和代码中整理它,最后,它(几乎)完成了。我女朋友一直不相信我在做什么,但我有证据!

    在伪代码中逐步分享我的解决方案,以帮助其他人避免类似的问题。这个答案更像是教程=)(真的吗?)。如果您有兴趣从头开始学习 WPF 时最复杂的事情是什么,我会说——让这一切真正成为 MVVM 和 f*g TreeView 绑定!

    如果您想要一个带有解决方案的存档文件,我可以稍后再给它,就在我做出决定时,这是值得的。一个限制,我不确定我是否可以共享 MINT.dll,它会带来 Actions,因为它是由我们公司的客户开发的。但我可以将其删除,然后共享应用程序,该应用程序只能显示有关 Fixtures 和 Tests 的信息,而不能显示有关操作的信息。

    夸夸其谈。只需要一点 C#/WinForms/HTML 背景和没有实践,我已经能够在将近 1 周内实现这个版本的应用程序(并写下这篇文章)。所以,不可能是可能的!像我一样放个假,好好学习WPF吧!

    分步教程(还没有附加文件)

    任务的简短重复

    前段时间我有一个任务来开发一个应用程序,它可以解析一个 DLL(它包含测试夹具、测试方法和动作 - 我们基于单元测试的自动化框架的单元),并以人类可读的形式导出它的结构格式(TXT、HTML、CSV、XML 等)。我决定使用 WPF 和纯 MVVM 来实现它(两者对我来说都是全新的东西)。对我来说最困难的两个问题是 MVVM 方法本身,然后是 MVVM 绑定到 TreeView 控件。我跳过了关于 MVVM 划分的部分,这是单独文章的主题。下面的步骤是关于以 MVVM 方式绑定到 TreeView 的。

    1. 不那么重要:创建 DLL,它可以通过单元测试打开 DLL,并使用反射找到夹具、测试方法和操作(更小级别的单元测试,在我们公司编写)。如果您对它是如何完成的感兴趣,请看这里:Parsing function / method content using Reflection
    2. DLL:为固定装置、测试和操作(数据模型、实体模型?)创建单独的类。我们将使用它们进行绑定。您应该自己思考,您的树的实体模型将是什么。主要思想 - 树的每个级别都应该由适当的类公开,这些属性可以帮助您在树中表示模型(并且,理想情况下,将在您的 MVVM 中正确放置,作为模型或模型的一部分)。就我而言,我对实体名称、子级列表和序号感兴趣。序号是一个数字,它代表一个实体在DLL内部代码中的顺序。它帮助我在 TreeView 中显示序数,仍然不确定它是正确的方法,但它有效!
    public class MintFixutre : IMintEntity
    {
        private readonly string _name;
        private readonly int _ordinalNumber;
        private readonly List<MintTest> _tests = new List<MintTest>();
        public MintFixutre(string fixtureName, int ordinalNumber)
        {
            _name = fixtureName;
            if (ordinalNumber <= 0)
                throw new ArgumentException("Ordinal number must begin from 1");
            _ordinalNumber = ordinalNumber;
        }
        public List<MintTest> Tests
        {
            get { return _tests; }
        }
        public string Name { get { return _name; }}
        public bool IsParent { get { return true; }  }
        public int OrdinalNumber { get { return _ordinalNumber; } }
    }
    
    public class MintTest : IMintEntity
    {
        private readonly string _name;
        private readonly int _ordinalNumber;
        private readonly List<MintAction> _actions = new List<MintAction>();
        public MintTest(string testName, int ordinalNumber)
        {
            if (string.IsNullOrWhiteSpace(testName))
                throw new ArgumentException("Test name cannot be null or space filled");
            _name = testName;
            if (ordinalNumber <= 0)
                throw new ArgumentException("OrdinalNumber must begin from 1");
            _ordinalNumber = ordinalNumber;
        }
        public List<MintAction> Actions
        {
            get { return _actions; }
        }
        public string Name { get { return _name; } }
        public bool IsParent { get { return true; } }
        public int OrdinalNumber { get { return _ordinalNumber; } }
    }
    
    public class MintAction : IMintEntity
    {
        private readonly string _name;
        private readonly int _ordinalNumber;
        public MintAction(string actionName, int ordinalNumber)
        {
            _name = actionName;
            if (ordinalNumber <= 0)
                throw new ArgumentException("Ordinal numbers must begins from 1");
            _ordinalNumber = ordinalNumber;
    
        }
        public string Name { get { return _name; } }
        public bool IsParent { get { return false; } }
        public int OrdinalNumber { get { return _ordinalNumber; } }
    }
    

    顺便说一句,我还在下面创建了一个接口,它实现了所有实体。这样的界面可以在将来为您提供帮助。仍然不确定,我是否也应该在那里添加List&lt;IMintEntity&gt; 类型的Childrens 属性,或者类似的东西?

    public interface IMintEntity
    {
        string Name { get; }
        bool IsParent { get; }
        int OrdinalNumber { get; }
    }
    
    1. DLL - 构建数据模型:DLL 有一个方法可以通过单元测试和枚举数据打开 DLL。在枚举过程中,它会构建如下所示的数据模型。给出了真实的方法示例,使用了反射核心+Mono.Reflection.dll,不要与复杂性相混淆。您所需要的一切 - 看看该方法如何用实体填充 _fixtures 列表。
    private void ParseDllToEntityModel()
    {
        _fixutres = new List<MintFixutre>();
    
        // enumerating Fixtures
        int f = 1;
        foreach (Type fixture in AssemblyTests.GetTypes().Where(t => t.GetCustomAttributes(typeof(TestFixtureAttribute), false).Length > 0))
        {
            var tempFixture = new MintFixutre(fixture.Name, f);
    
            // enumerating Test Methods
            int t = 1;
            foreach (var testMethod in fixture.GetMethods().Where(m => m.GetCustomAttributes(typeof(TestAttribute), false).Length > 0))
            {
                // filtering Actions
                var instructions = testMethod.GetInstructions().Where(
                    i => i.OpCode.Name.Equals("newobj") && ((ConstructorInfo)i.Operand).DeclaringType.IsSubclassOf(typeof(BaseAction))).ToList();
    
                var tempTest = new MintTest(testMethod.Name, t);
    
                // enumerating Actions
                for ( int a = 1; a <= instructions.Count; a++ )
                {
                    Instruction action = instructions[a-1];
                    string actionName = (action.Operand as ConstructorInfo).DeclaringType.Name;
                    var tempAction = new MintAction(actionName, a);
                    tempTest.Actions.Add(tempAction);
                }
    
                tempFixture.Tests.Add(tempTest);
                t++;
            }
    
            _fixutres.Add(tempFixture);
            f++;
        }
    }
    
    1. DLL:创建List&lt;MintFixutre&gt; 类型的公共属性Fixtures 以返回刚刚创建的数据模型(夹具列表,其中包含测试列表,其中包含操作列表)。这将是我们对TreeView 的绑定源。
    public List<MintFixutre> Fixtures
    {
        get { return _fixtures; }
    }
    
    1. MainWindow 的 ViewModel(内部带有 TreeView):包含来自 DLL 的对象/类,可以解析单元测试 DLL。还从List&lt;MintFixutre&gt; 类型的DLL 公开Fixtures 公共属性。我们将从 MainWindow 的 XAML 绑定到它。类似的东西(简化):
    var _exporter = MySuperDllReaderExporterClass ();
    // public property of ViewModel for TreeView, which returns property from #4
    public List<MintFixture> Fixtures { get { return _exporter.Fixtures; }}
    // Initializing exporter class, ParseDllToEntityModel() is called inside getter 
    // (from step #3). Cool, we have entity model for binding.
    _exporter.PathToDll = @"open file dialog can help";
    // Notifying all those how are bound to the Fixtures property, there are work for them, TreeView, are u listening?
    // will be faced later in this article, anticipating events
    OnPropertyChanged("Fixtures");
    
    1. MainWindow 的 XAML - 设置数据模板:在包含 TreeView 的 Grid 中,我们创建 &lt;Grid.Resources&gt; 部分,其中包含一组用于我们的 TreeViewItems 的模板。 HierarchicalDataTemplate(Fixtures and Tests)用于那些有子项的人,DataTemplate 用于“叶”项(动作)。对于每个模板,我们指定其内容(文本、TreeViewItem 图像等)、ItemsSource(如果此项目有子项,例如对于 Fixtures,它是 {Binding Path=Tests})和 ItemTemplate(同样,仅在此项目有孩子,这里我们设置模板之间的链接——FixtureTemplate 使用 TestTemplate 作为它的孩子,TestTemplate 使用 ActionTemplate 作为它的孩子,Action 模板不使用任何东西,它是一片叶子!)。重要提示:不要忘记,为了将“一个”模板“链接”到“另一个”,“另一个”模板必须在 XAML 中定义在“一个”之上! (只是列举我自己的错误:))

    2. XAML - TreeView 链接:我们将 TreeView 设置为:与来自 ViewModel 的数据模型(还记得公共属性吗?)和刚刚准备好的模板链接,这些模板代表内容、外观、数据源和树的嵌套项目!还有一个重要的注意事项。不要将您的 ViewModel 定义为 XAML 中的“静态”资源,例如 &lt;Window.Resources&gt;&lt;MyViewModel x:Key="DontUseMeForThat" /&gt;&lt;/Window.Resources&gt;。如果您这样做,那么您将无法在财产发生变化时通知它。为什么?静态资源是静态资源,它初始化一个,然后保持不可变。我在这里可能错了,但这是我的错误之一。所以对于 TreeView 使用 ItemsSource="{Binding Fixtures}" 而不是 ItemsSource="{StaticResource myStaticViewModel}"

    3. ViewModel - ViewModelBase - 属性已更改:几乎所有。停止!当用户打开应用程序时,最初 TreeView 当然是空的,因为用户还没有打开任何 DLL!我们必须等到用户打开一个DLL,然后才执行绑定。它是通过OnPropertyChanged 事件完成的。为了让生活更轻松,我所有的 ViewModel 都继承自 ViewModelBase,它正确地将这个功能公开给我的所有 ViewModel。

    public class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
    
        protected virtual void OnPropertyChanged(string propertyName)
        {
            OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
        }
    
        protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
        {
            var handler = PropertyChanged;
            if (handler != null)
                handler(this, args);
        }        
    }
    
    1. XAML - OnPropertyChanged 和命令。用户单击按钮打开包含单元测试数据的 DLL。当我们使用MVVM 时,点击是通过命令处理的。在OpenDllExecuted 处理程序OnPropertyChanged("Fixtures") 的末尾执行,通知树,它绑定到的属性已更改,现在是时候刷新自己了。 RelayCommand helper 类可以取自 there)。顺便说一句,据我所知,存在一些帮助程序库和工具包在 XAML 中发生了类似的事情:

    2. 和 ViewModel - 命令

    private ICommand _openDllCommand;
    
            //...
    
    public ICommand OpenDllCommand
    {
        get { return _openDllCommand ?? (_openDllCommand = new RelayCommand(OpenDllExecuted, OpenDllCanExecute)); }
    }
    
            //...
    
    // decides, when the <OpenDll> button is enabled or not
    private bool OpenDllCanExecute(object obj)
    {
        return true; // always true for Open DLL button
    }
    
            //...
    
    // in fact, handler
    private void OpenDllExecuted(object obj)
    {
        var openDlg = new OpenFileDialog { ... };
        _pathToDll = openDlg.FileName;
        _exporter.PathToDll = _pathToDll;
                    // Notifying TreeView via binding that the property <Fixtures> has been changed,
                    // thereby forcing the tree to refresh itself
        OnPropertyChanged("Fixtures");
    }
    
    1. 最终用户界面(但对我来说不是最终的,很多事情都应该做!)。在某处使用了扩展的 WPF 工具包:http://wpftoolkit.codeplex.com/

    【讨论】:

      猜你喜欢
      • 2014-08-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-03-01
      • 1970-01-01
      • 2011-10-22
      相关资源
      最近更新 更多