WPF:使用MVVM方式逐步教程绑定TreeView

9
请看下一篇文章。这篇原始的问题内容已被删除,因为没有任何意义。简单来说,我问如何使用XmlDataProvider在MVVM方式中将我错误地解析DLL程序集生成的XML绑定到TreeView。但后来我意识到这种方法是错误的,所以我转而生成数据实体模型(只需编写代表我想在树中公开的所有实体的类)。因此,结果在下一篇文章中。目前,我不时更新这篇“文章”,所以按F5键即可。
享受阅读!
1个回答

24

介绍

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

这是一个漫长的故事,大多数人可以跳过它 :) 但那些想要了解问题和解决方案的人必须全部阅读!

我是QA,一段时间以前负责产品自动化的工作。幸运的是,这种自动化不是在某个测试工具中进行,而是在Visual Studio中进行,因此与开发最为接近。

我们使用的自动化框架由MbUnit(Gallio作为运行器)和MINT(我们合作的客户编写的MbUnit补充)组成。MbUnit为我们提供了测试夹具和测试,而MINT添加了额外的小层-测试中的操作。例如,夹具称为“FilteringFixture”。它包括像“TestingFilteringById”或“TestingFilteringWithSpecialChars”之类的多个测试。每个测试由操作组成,这些操作是我们测试的原子单位。操作的示例包括-“打开应用程序(参数)”,“打开过滤对话框”等。

我们已经有很多测试,其中包含很多操作,乱七八糟。它们使用产品QA的内部API。此外,我们开始调查一种新的自动化方法——通过Microsoft UI Automation进行UI自动化(抱歉,这是重复的)。因此,对于经理来说,“导出器”或“报告工具”的必要性变得非常严重。
一段时间以前,我得到了一个任务:开发一个可以解析DLL(其中包含所有fixture、测试和操作)并将其结构导出为人类可读格式(TXT、HTML、CSV、XML或其他任何格式)的应用程序。但是,紧接着我就去度假了(2周)。
恰好在那段时间里,我的女朋友也去度假了(她也请了假),我一个人留在家里。想着如何度过这段时间(2周),我想起了那个“编写导出器工具”的任务,以及我一直计划开始学习WPF的长期计划。所以,我决定在度假期间完成我的任务,并将一个应用程序改成WPF。那时我听说过MVVM,我决定使用纯MVVM实现它。
可以解析DLL等内容的DLL编写得相当快(约1-2天)。之后,我开始使用WPF,本文将向您展示它的最终结果。
我度过了假期的大部分时间(将近8天!)试图在自己的头脑和代码中整理出来,最终,它完成了(几乎)。我的女朋友不会相信我这段时间在做什么,但我有证据!
逐步以伪代码分享我的解决方案,以帮助其他人避免类似的问题。这个答案更像是教程=)(真的吗?)。如果你想知道从零开始学习WPF时最复杂的事情是什么,我会说——让它真正成为MVVM和f*g TreeView绑定!
如果你想要一个包含解决方案的归档文件,我可以稍后提供,只要我做出决定,认为它值得。一个限制是,我不确定是否可以分享MINT.dll,它带来了Actions,因为它是我们公司的客户开发的。但我可以将其删除,并分享应用程序,该应用程序只能显示有关夹具和测试的信息,而不能显示有关操作的信息。
自夸之言。只需要一点C# / WinForms / HTML背景和没有实践,我已经能够在将近1周的时间内实现这个应用程序的版本(并写下这篇文章)。所以,不可能变为可能!就像我一样放假,花时间学习WPF!

逐步教程(暂无附带文件)

任务简述:

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

  1. 不太重要:创建一个可以使用反射打开带有单元测试的DLL并查找fixture、测试方法和操作(更小级别的单元测试,由我们公司编写)。如果您对如何完成此操作感兴趣,请查看这里:使用反射解析函数/方法内容
  2. DLL:为fixture、测试和操作(数据模型、实体模型?)创建了分离的类。我们将使用它们进行绑定。您应该自己考虑树的实体模型是什么。主要思想是,树的每个级别都应该由适当的类公开,具有那些属性,可以帮助您在树中表示模型(并且在理想情况下,将在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<IMintEntity>类型的Childrens属性或类似的内容?

public interface IMintEntity
{
    string Name { get; }
    bool IsParent { get; }
    int OrdinalNumber { get; }
}

3. 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++;
    }
}

4. DLL:创建公共属性Fixtures,类型为List<MintFixture>,以返回刚刚创建的数据模型(包含测试列表和操作列表的夹具列表)。这将成为我们TreeView的绑定源。
public List<MintFixutre> Fixtures
{
    get { return _fixtures; }
}

ViewModel of MainWindow(内部包含TreeView):包含可以解析单元测试DLL的对象/类,还公开来自List<MintFixutre>类型的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");

6. MainWindow的XAML - 设置数据模板: 在一个包含TreeView的Grid内,我们创建了一个<Grid.Resources>部分,其中包含一组用于我们的TreeViewItem的模板。对于那些有子项的项目(Fixtures和Tests),我们使用HierarchicalDataTemplate,对于“叶子”项目(Actions),我们使用DataTemplate。对于每个模板,我们指定其内容(文本、TreeViewItem图像等)、ItemsSource(如果此项具有子项,例如对于Fixtures它是{Binding Path=Tests})和ItemTemplate(仅在此项具有子项的情况下,在这里我们设置模板之间的链接——FixtureTemplate使用TestTemplate作为其子项,TestTemplate使用ActionTemplate作为其子项,Action模板不使用任何内容,它是一个叶子!)。重要提示:不要忘记,为了将“一个”模板“链接”到“另一个”模板,“另一个”模板必须在XAML中定义在“一个”模板之上!(只是列举我自己的错误 :))
7. XAML - TreeView链接:我们使用ViewModel中的数据模型链接TreeView,并使用刚刚准备好的模板表示树形项的内容、外观、数据源和嵌套!还有一个重要的注意点。不要将您的ViewModel定义为XAML中的“静态”资源,例如<Window.Resources><MyViewModel x:Key="DontUseMeForThat" /></Window.Resources>。如果这样做,那么您将无法通知它属性更改。为什么?静态资源是静态资源,它只初始化一次,之后保持不变。我可能在这里错了,但这是我的一个错误。因此,对于TreeView,请使用ItemsSource="{Binding Fixtures}"而不是ItemsSource="{StaticResource myStaticViewModel}" 8. ViewModel - ViewModelBase - Property Changed: 几乎全部都完成了。等等!当用户打开应用程序时,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);
    }        
}

9. XAML - OnPropertyChanged和命令。用户点击按钮以打开包含单元测试数据的DLL文件。由于我们使用了MVVM,因此点击通过命令处理。在OpenDllExecuted处理程序的末尾,执行OnPropertyChanged("Fixtures"),通知树形结构,绑定到其上的属性已更改,现在是刷新自身的时候了。可以从这里中获取RelayCommand帮助类的示例。顺便说一句,据我所知,存在一些帮助库和工具包。类似于XAML中发生的情况如下:
  • 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");
    }
    

    11. 最终用户界面(但对我来说不是最终的,还有很多事情要做!)。在某些地方使用了扩展的WPF工具包:http://wpftoolkit.codeplex.com/


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接