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

tri*_*kbz 9 wpf treeview xaml binding mvvm

看下一篇文章.原始的一个问题内容已被删除,因为没有任何意义.简单地说,我问过如何使用MVVM方式的XmlDataProvider将XML(我在解析DLL程序集时错误生成)绑定到TreeView.但后来我明白这种方法是错误的,我转而生成数据实体模型(只编写表示我想在树中公开的所有实体的类)而不是XML.

那么,结果在下一篇文章中.目前我不时更新这篇"文章",所以F5和

享受阅读!

tri*_*kbz 24

介绍

我读过这篇文章的正确方法

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

我是QA,前段时间已经对我点击的产品的自动化负责.幸运的是,这个自动机不是在某些测试工具中,而是在Visual Studio中,所以它最接近开发.

对于我们的自动化,我们使用一个框架,该框架由MbUnit(Gallio作为转轮)和MINT(MbUnit的添加,由我们合作的客户编写)组成.MbUnit为我们提供了测试夹具和测试,MINT增加了更小的层 - 测试中的动作.例.Fixture被称为'FilteringFixture'.它由大量的测试组成,如'TestingFilteringById'或'TestingFilteringWithSpecialChars'等.每个测试都包含动作,这些动作是我们测试的原子单元.操作示例包括 - '打开应用程序(参数)','OpenFilterDialog'等.

我们已经进行了很多测试,其中包含很多动作,这很麻烦.他们使用我们QA产品的内部API.此外,我们开始研究一种新的自动化方法 - 通过Microsoft UI Automation进行UI自动化(对于重言式抱歉).因此,一些"出口商"或"记者"工具的必要性对管理者来说变得非常严重.

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

事情发生了,我的女朋友去了她的家人,直到休假(她也得到了),我一个人呆在家里.想想我一直在做什么(2周),我记得那个"写出口工具任务"以及我多长时间计划开始学习WPF.所以,我决定在休假期间完成我的任务,并为WPF打扮一个应用程序.那时我听说过MVVM,我决定用纯MVVM实现它.

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

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

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

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

自夸的话语.只需要一点C#/ WinForms/HTML背景并且没有练习,我已经能够在近一周内实现这个版本的应用程序(并撰写本文).所以,不可能!和我一样度假,把它花在WPF学习上!

一步一步的教程(没有附加文件)

任务的重复次数很少:

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

  1. 不那么重要:创建DLL,可以使用单元测试打开DLL,并使用反射查找夹具,测试方法和操作(更小的单元测试级别,在我们公司编写).如果您对如何完成感兴趣,请查看此处:使用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; } }
}
Run Code Online (Sandbox Code Playgroud)

顺便说一句,我还在下面创建了一个实现所有实体的接口.这样的界面可以在将来帮助您.仍然不确定,我是否还要添加类型的Childrens属性List<IMintEntity>,或类似的东西?

public interface IMintEntity
{
    string Name { get; }
    bool IsParent { get; }
    int OrdinalNumber { get; }
}
Run Code Online (Sandbox Code Playgroud)
  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++;
    }
}
Run Code Online (Sandbox Code Playgroud)
  1. DLL:创建FixturesList<MintFixutre>类型的公共属性以返回刚刚创建的数据模型(Fixtures列表,其中包含测试列表,其中包含Actions列表).这将是我们的绑定来源TreeView.
public List<MintFixutre> Fixtures
{
    get { return _fixtures; }
}
Run Code Online (Sandbox Code Playgroud)
  1. MainWindow的ViewModel(内置TreeView):包含DLL中的对象/类,可以解析单元测试DLL.还FixturesList<MintFixutre>类型的DLL 公开公共属性.我们将从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");
Run Code Online (Sandbox Code Playgroud)
  1. MainWindow的XAML - 设置数据模板:在包含TreeView的网格内部,我们创建了一个<Grid.Resources>部分,其中包含一组模板TreeViewItem.HierarchicalDataTemplate(夹具和测试)用于拥有子项目的人,DataTemplate用于"叶子"项目(操作).对于每个模板,我们指定其内容(文本,TreeViewItem图像等),ItemsSource(如果此项目具有子项,例如对于Fixtures {Binding Path=Tests}),以及ItemTemplate(再次,仅在此项目有子项的情况下,此处我们设置模板之间的链接 - FixtureTemplate为其子节点使用TestTemplate,TestTemplate为其子节点使用ActionTemplate,Action模板不使用任何东西,它是一个叶子!).重要提示:不要忘记,为了将"一个"模板"链接"到"另一个",必须在"一个"上方的XAML中定义"另一个"模板!(只是列举我自己的错误:))

  2. XAML - TreeView链接:我们设置TreeView:连接ViewModel中的数据模型(记住公共属性?)和刚刚准备好的模板,它们代表树项的内容,外观,数据源和嵌套!一个更重要的说明.不要将ViewModel定义为XAML中的"静态"资源,例如<Window.Resources><MyViewModel x:Key="DontUseMeForThat" /></Window.Resources>.如果您这样做,那么您将无法在更改的财产上通知它.为什么?静态资源是静态资源,它初始化一个,然后保持不可变.我可能在这里错了,但这是我的错误之一.所以对于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);
    }        
}
Run Code Online (Sandbox Code Playgroud)
  1. XAML - OnPropertyChanged和命令.用户单击按钮以打开包含单元测试数据的DLL.正如我们使用的那样MVVM,点击是通过命令来处理的.在OpenDllExecuted处理程序结束时OnPropertyChanged("Fixtures"),通知Tree,它所绑定的属性已被更改,现在是时候刷新自己了.RelayCommand辅助类可以从那里获取).顺便说一句,据我所知,有一些帮助库和工具包存在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");
}
Run Code Online (Sandbox Code Playgroud)
  1. 最终用户界面(但对我来说不是最终的,应该做很多事情!).在某处使用了扩展的WPF工具包:http://wpftoolkit.codeplex.com/