WPF:在 MVVM 中绑定 TreeView 的分步教程
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/13789563/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
WPF: Binding TreeView in MVVM way step by step tutorial
提问by trickbz
See the next post. This original one question content has been removed, as doesn't have any sense. Briefly, I asked how to bind XML (which I generated by mistake while parsing DLL assembly) to TreeView using XmlDataProvider in MVVM way. But later I understood that this approach was wrong, and I switched to generation of data entity model (just write classes which represent all the entities I would like to expose in the tree) instead of XML.
见下一个帖子。原来的一题内容已被删除,因为没有任何意义。简而言之,我询问了如何使用 XmlDataProvider 以 MVVM 方式将 XML(我在解析 DLL 程序集时错误生成)绑定到 TreeView。但后来我明白这种方法是错误的,我转而使用数据实体模型的生成(只需编写代表我想在树中公开的所有实体的类)而不是 XML。
So, the result in the next post. Currently from time to time I update this "article", so F5, and
所以,结果在下一篇文章中。目前我不时更新这篇“文章”,所以 F5,和
Enjoy reading!
享受阅读!
回答by trickbz
Introduction
介绍
The right way I had found reading thisarticle
我找到了阅读这篇文章的正确方法
It's a long story, most of you just can skip it :) But those, who want to understand the problem and solution, must read this all !
这是一个很长的故事,你们大多数人可以跳过它:) 但是那些想要了解问题和解决方案的人必须阅读这一切!
I'm QA, and some time ago had become responsible for Automation of the product I clicks. Fortunately, this automaton takes place not in some Testing Tool, but in Visual Studio, so it is maximally close to development.
我是 QA,前段时间负责我点击的产品的自动化。幸运的是,这个自动机不是发生在一些测试工具中,而是发生在 Visual Studio 中,因此它最接近开发。
For our automation we use a framework which consist of MbUnit (Gallio as runner) and of MINT (addition to MbUnit, which is written by the customer we work with). MbUnit gives us Test Fixtures and Tests, and MINT adds additional smaller layer -- Actions inside tests. Example. Fixture is called 'FilteringFixture'. It consist of amount of tests like 'TestingFilteringById', or 'TestingFilteringWithSpecialChars', etc. Each test consist of actions, which are atomic unit of our test. Example of actions are - 'Open app (parameter)', 'OpenFilterDialog', etc.
对于我们的自动化,我们使用由 MbUnit(Gallio 作为运行程序)和 MINT(添加到 MbUnit,由我们合作的客户编写)组成的框架。MbUnit 为我们提供了 Test Fixtures 和 Tests,而 MINT 添加了额外的较小层——测试中的 Actions。例子。夹具称为“FilteringFixture”。它由大量测试组成,如“TestingFilteringById”或“TestingFilteringWithSpecialChars”等。每个测试都由操作组成,这些操作是我们测试的原子单元。操作示例有 -“打开应用程序(参数)”、“OpenFilterDialog”等。
We already have a lot of tests, which contain a lot of actions, it's a mess. They use internal API of the product we QA. Also, we start investigation a new Automation approach - UI automation via Microsoft UI Automation (sorry for tautology). So the necessity of some "exporter", or "reporter" tool became severe for managers.
我们已经有很多测试,其中包含很多操作,很混乱。他们使用我们 QA 产品的内部 API。此外,我们开始研究一种新的自动化方法 - 通过 Microsoft UI 自动化进行的 UI 自动化(对不起,同义反复)。因此,对于管理人员来说,某些“导出器”或“报告器”工具的必要性变得非常重要。
Some time ago I have got a task to develop some application, which can parse a DLL (which contains all the fixtures, tests and actions), and export its structure in the human readable format (TXT, HTML, CSV, XML, any other). But, right after that, I went to vacation (2 weeks).
前段时间我有一个任务来开发一些应用程序,它可以解析一个 DLL(它包含所有的装置、测试和动作),并以人类可读的格式(TXT、HTML、CSV、XML、任何其他格式)导出其结构)。但是,在那之后,我去度假(2周)。
It happens so, that my girlfriend went to her family until vacation (she also got it), and I remained at home so alone. Thinking what me to do all this time (2 weeks), I remember about that "write exporter tool task" and how long I have been planning to start learning WPF. So, I decided to make my task during vacation, and also dress a application to WPF. At that time I heard something about MVVM, and I decided to implement it using pure MVVM.
事情是这样的,我的女朋友去她家直到假期(她也得到了),而我一个人呆在家里。一直在思考我要做什么(2 周),我记得关于“编写导出器工具任务”以及我计划开始学习 WPF 的时间。所以,我决定在假期完成我的任务,并为 WPF 编写一个应用程序。当时听说了MVVM,决定用纯MVVM来实现。
DLL which can parse DLL with fixrtures etc had been written rather fast (~1-2 days). After that I had started with WPF, and this article will show you how it ended.
可以用 fixrtures 等解析 DLL 的 DLL 编写得相当快(~1-2 天)。之后我开始使用 WPF,本文将向您展示它是如何结束的。
I have spent a major part of my vacation (almost 8 days!), trying to sorted it out in my head and code, and finally, it is done (almost). My girlfriend would not believe what I was doing all this time, but I have a proof!
我花了大部分假期(将近 8 天!),试图在我的头脑和代码中整理它,最后,它(几乎)完成了。我的女朋友不会相信我一直在做什么,但我有证据!
Sharing my solution step by step in pseudo code, to help others avoid similar problems. This answer is more looks like tutorial =) (Really?). If you are interested what were the most complicated things while learning WPF from scratch, I would say -- make it all really MVVM and f*g TreeView binding!
用伪代码一步一步分享我的解决方案,帮助其他人避免类似的问题。这个答案更像是教程 =)(真的吗?)。如果您对从头开始学习 WPF 时最复杂的事情感兴趣,我会说 - 让它成为真正的 MVVM 和 f*g TreeView 绑定!
If you want an archived file with solution, I can give it a bit later, just when I have made a decision, that it is worth of that. One limitation, I'm not sure I may share the MINT.dll, which brings Actions, as it has been developed by the customer of our company. But I can just remove it, and share the application, which can display information about Fixtures and Tests only, but not about actions.
如果你想要一个带有解决方案的存档文件,我可以稍后给它,就在我做出决定时,它是值得的。一个限制,我不确定我是否可以共享 MINT.dll,它带来了 Actions,因为它是由我们公司的客户开发的。但是我可以删除它,然后共享该应用程序,该应用程序只能显示有关 Fixtures 和 Tests 的信息,而不能显示有关操作的信息。
Boastful words. With just a little C# / WinForms / HTML background and no practice I have been able to implement this version of the application in almost 1 week (and write this article). So, impossible is possible! Just take a vacation like me, and spend it to WPF learning!
豪言壮语。只需要一点 C#/WinForms/HTML 背景并且没有练习,我已经能够在将近 1 周内实现这个版本的应用程序(并写下这篇文章)。所以,不可能是可能的!就和我一样放个假,花在WPF学习上吧!
Step by step tutorial (w/o attached files yet)
分步教程(还没有附加文件)
Short repetition of the task:
任务的简短重复:
Some time ago I have got a task to develop an application, which can parse a DLL (which contains test fixtures, test methods and actions - units of our unit testing based automation framework), and export its structure in the human readable format (TXT, HTML, CSV, XML, any other). I decided to implement it using WPF and pure MVVM (both were absolutely new things for me). The 2 most difficult problems for me became MVVM approach itself, and then MVVM binding to TreeView control. I skip the part about MVVM division, it's a theme for separate article. The steps below are about binding to TreeView in MVVM way.
前段时间我有一个任务来开发一个应用程序,它可以解析一个 DLL(它包含测试装置、测试方法和操作 - 我们基于单元测试的自动化框架的单元),并以人类可读的格式(TXT)导出其结构、HTML、CSV、XML、任何其他)。我决定使用 WPF 和纯 MVVM 来实现它(这两者对我来说都是全新的东西)。对我来说,两个最困难的问题是 MVVM 方法本身,然后是 MVVM 绑定到 TreeView 控件。我跳过了关于 MVVM 划分的部分,这是单独文章的主题。下面的步骤是关于以 MVVM 方式绑定到 TreeView。
- Not so important: Create DLL which can open DLL with unit tests and finds fixtures, test methods and actions (more smaller level of unit test, written in our company) using reflection. If you are interested in how it had been done, look here: Parsing function / method content using Reflection
- DLL: Separated classes are created for both fixtures, tests and actions (data model, entity model?).We'll use them for binding. You should think by yourself, what will be an entity model for your tree. Main idea - each level of tree should be exposed by appropriate class, with those properties, which help you to represent the model in the tree (and, ideally, will take right place in your MVVM, as model or part of the model). In my case, I was interested in entity name, list of children and ordinal number. Ordinal number is a number, which represents order of an entity in the code inside DLL. It helps me show ordinal number in the TreeView, still not sure it's right approach, but it works!
- 不那么重要:创建可以使用单元测试打开 DLL 的 DLL,并使用反射查找装置、测试方法和操作(更小的单元测试级别,在我们公司编写)。如果您对它是如何完成的感兴趣,请看这里:使用反射解析函数/方法内容
- 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; } }
}
BTW, I also created an interface below, which implement all the entities. Such interface can help you in the future. Still not sure, should I was also add there Childrens
property of List<IMintEntity>
type, or something like that?
顺便说一句,我还在下面创建了一个接口,它实现了所有实体。这样的界面可以在未来为您提供帮助。仍然不确定,我是否还应该添加typeChildrens
属性List<IMintEntity>
或类似的属性?
public interface IMintEntity
{
string Name { get; }
bool IsParent { get; }
int OrdinalNumber { get; }
}
- DLL - building data model: DLL has a method which opens DLL with unit tests and enumerating data. During enumeration, it builds a data model like below. Real method example is given, reflection core + Mono.Reflection.dll are used, don't be confused with complexity. All that you need - look how the method fills
_fixtures
list with entities.
- 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++;
}
}
- DLL: Public property
Fixtures
of theList<MintFixutre>
type is created to return just created data model ( List of Fixtures, which contain lists of tests, which contains lists of Actions ). This will be our binding source forTreeView
.
- DLL:公共属性
Fixtures
的的List<MintFixutre>
类型创建回到刚刚创建数据模型(灯具,其中包含的测试,其中包含操作的列表列表的列表)。这将是我们的绑定源TreeView
。
public List<MintFixutre> Fixtures
{
get { return _fixtures; }
}
- ViewModel of MainWindow (with TreeView inside): Contains object / class from DLL which can parse unit tests DLLs. Also exposes
Fixtures
public property from the DLL ofList<MintFixutre>
type. We will bind to it from XAML of MainWindow. Something like that (simplified):
- MainWindow 的 ViewModel(里面有 TreeView):包含来自 DLL 的对象/类,它可以解析单元测试 DLL。还
Fixtures
从List<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");
XAML of MainWindow - Setup data templates: Inside a Grid, which contains TreeView, we create
<Grid.Resources>
section, which contains a set of templates for ourTreeViewItem
s.HierarchicalDataTemplate
(Fixtures and Tests) is used for those who have child items, andDataTemplate
is used for "leaf" items (Actions). For each template, we specify which its Content (text, TreeViewItem image, etc.), ItemsSource (in case of this item has children, e.g. for Fixtures it is{Binding Path=Tests}
), and ItemTemplate (again, only in case this item has children, here we set linkage between templates - FixtureTemplate uses TestTemplate for its children, TestTemplate uses ActionTemplate for its children, Action template does not use anything, it is a leaf!). IMPORTANT: Don't forget, that in order to "link" "one" template to "another", the "another" template must be defined in XAML above the "one"! (just enumerating my own blunders :) )XAML - TreeView linkage: We setup TreeView with: linking with data model from ViewModel (remember public property?) and with just prepared templates, which represent content, appearance, data sources and nesting of tree items! One more important note. Don't define your ViewModel as "static" resource inside XAML, like
<Window.Resources><MyViewModel x:Key="DontUseMeForThat" /></Window.Resources>
. If you do so, then you won't be able to notify it on property changed. Why? Static resource is static resource, it initializes ones, and after that remains immutable. I might be wrong here, but it was one of my blunders. So for TreeView useItemsSource="{Binding Fixtures}"
instead ofItemsSource="{StaticResource myStaticViewModel}"
ViewModel - ViewModelBase - Property Changed: Almost all. Stop! When user opens an application, then initially TreeView is empty of course, as user hasn't opened any DLL yet! We must wait until user opens a DLL, and only then perform binding. It is done via
OnPropertyChanged
event. To make life easier, all my ViewModels are inherited from ViewModelBase, which right exposes this functionality to all my ViewModel.
MainWindow 的 XAML - 设置数据模板:在包含 TreeView 的 Grid 中,我们创建
<Grid.Resources>
部分,其中包含我们的TreeViewItem
s 的一组模板。HierarchicalDataTemplate
(Fixtures and Tests) 用于具有子项的那些,并DataTemplate
用于“叶”项 (Actions)。对于每个模板,我们指定其内容(文本、TreeViewItem 图像等)、ItemsSource(如果此项目有子项,例如对于 Fixtures,它是{Binding Path=Tests}
) 和 ItemTemplate(同样,只有在这个 item 有子元素的情况下,这里我们设置模板之间的链接 - FixtureTemplate 为其子元素使用 TestTemplate,TestTemplate 为其子元素使用 ActionTemplate,Action 模板不使用任何东西,它是一片叶子!)。重要提示:不要忘记,为了将“一个”模板“链接”到“另一个”,“另一个”模板必须在 XAML 中定义在“一个”之上!(只是列举我自己的错误:))XAML - TreeView 链接:我们使用以下方式设置 TreeView:与来自 ViewModel 的数据模型链接(还记得公共属性吗?)和刚刚准备好的模板,这些模板表示内容、外观、数据源和树项的嵌套!一个更重要的注意事项。不要将您的 ViewModel 定义为 XAML 中的“静态”资源,例如
<Window.Resources><MyViewModel x:Key="DontUseMeForThat" /></Window.Resources>
. 如果您这样做,那么您将无法在属性更改时通知它。为什么?静态资源是静态资源,它初始化一个,之后保持不变。我可能在这里错了,但这是我的失误之一。所以对于 TreeView 使用ItemsSource="{Binding Fixtures}"
而不是ItemsSource="{StaticResource myStaticViewModel}"
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);
}
}
XAML - OnPropertyChanged and commanding. User clicks a button to opens DLL which contains unit tests data. As we using
MVVM
, then click is handled via commanding. At the end of theOpenDllExecuted
handlerOnPropertyChanged("Fixtures")
is executed, notifying the Tree, that the property, to which it is bind to has been changed, and that now is time to refresh itself.RelayCommand
helper class can be taken for example from there). BTW, as I know, there are some helper libraries and toolkits exist Something like that happens in the XAML:And ViewModel - Commanding
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");
}
- Final UI(but not final for me, a lot of things should be done!). Extended WPF toolkit was used somewhere: http://wpftoolkit.codeplex.com/
- 最终的用户界面(但对我来说不是最终的,应该做很多事情!)。某处使用了扩展的 WPF 工具包:http: //wpftoolkit.codeplex.com/