打开文件对话框 MVVM
好的,我真的很想知道 MVVM 专家开发人员如何处理 WPF 中的 openfile 对话框。
我真的不想在我的 ViewModel 中执行此操作(其中“浏览”是通过 DelegateCommand 引用的),
void Browse(object param)
{
//Add code here
OpenFileDialog d = new OpenFileDialog();
if (d.ShowDialog() == true)
{
//Do stuff
}
}
因为我认为这违背了 MVVM 方法。
我该怎么办?
Ok I really would like to know how expert MVVM developers handle an openfile dialog in WPF.
I don't really want to do this in my ViewModel(where 'Browse' is referenced via a DelegateCommand)
void Browse(object param)
{
//Add code here
OpenFileDialog d = new OpenFileDialog();
if (d.ShowDialog() == true)
{
//Do stuff
}
}
Because I believe that goes against MVVM methodology.
What do I do?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(8)
长话短说:
解决方案是显示类中的用户交互,该类是视图组件的一部分。
这意味着,这样的类必须是视图模型未知的类,因此不能被视图模型调用。
当然,解决方案可以涉及代码隐藏实现,因为在评估解决方案是否符合MVVM时,代码隐藏并不相关。
除了回答原来的问题之外,这个答案还试图提供关于一般问题的替代视图,为什么从视图模型控制对话框等 UI 组件违反了 MVVM 设计模式,以及为什么对话框服务等解决方法不能解决问题。
1 MVVM 和对话框
1.1 对常见建议的批评
几乎所有的答案都遵循这样的误解:MVVM 是一种模式,它针对类级别依赖关系,并且需要 空的代码隐藏文件。 但这是一种架构模式,试图在应用程序/组件级别解决不同的问题:保持业务领域与 UI 分离。
大多数人(这里是 SO)都同意视图模型不应该处理对话框,但随后建议将 UI 相关逻辑移至辅助类(无论它称为辅助类还是服务),该类仍然由视图控制型号。
这(尤其是服务版本)也称为依赖关系隐藏。 此类模式被视为反模式。 服务定位器是最臭名昭著的依赖隐藏反模式。
这就是为什么我将任何涉及将 UI 逻辑从视图模型类提取到单独的类(在视图模型组件中)的模式称为反模式。 它没有解决最初的问题:如何更改应用程序结构或类设计,以便从视图模型(或模型)类中删除 UI 相关职责并将其移回视图相关类。
换句话说:关键逻辑仍然是视图模型组件的一部分。
因此,我不建议实施涉及对话服务(无论它是否隐藏在界面后面)的解决方案,例如已接受的解决方案。 如果您关心编写符合 MVVM 设计模式的代码,那么就不要在视图模型内处理对话框视图或 UI 定向消息传递。
引入一个接口来解耦类级依赖关系,例如 IFileDialogService 接口,称为依赖反转原则(SOLID 中的 D),与 MVVM 无关。 当它与MVVM无关时,它就无法解决MVVM相关问题。 当室温与建筑物是四层建筑还是摩天大楼无关时,改变室温永远无法将任何建筑物变成摩天大楼。
MVVM 不是依赖倒置 的同义词。
MVVM 是一种架构设计模式,而 依赖倒置 是一种 OO 语言原则,与构建应用程序(也称为软件架构)无关。 构造应用程序的不是接口(或抽象类型),而是抽象对象或实体(例如组件或模块),例如“模型 - 视图 - 视图模型”。 接口只能帮助“物理”地解耦模块。 它不会删除组件关联。
1.2 为什么对话框或处理
Window
总体感觉如此奇怪?我们必须记住,像
Microsoft.Win32.OpenFileDialog
这样的对话框控件是“低级”本机 Windows 控件。 他们没有必要的 API 来顺利集成到 MVVM 环境中。 由于其本质,它们在集成到 WPF 等高级框架中的方式存在一些限制。 一般来说,对话框或本机窗口主机是所有高级框架的已知“弱点”。对话框通常基于
Window
或抽象CommonDialog
类。Window
类是一个ContentControl
,因此允许样式和模板定位内容。一个很大的限制是,
Window
必须始终是根元素。 您无法将其作为子项添加到可视化树中,例如使用触发器显示/启动它或将其托管在DataTemplate
中。对于
CommonDialog
,它无法添加到可视化树中,因为它不扩展UIElement
。因此,基于
Window
或CommonDialog
的类型必须始终从代码隐藏中显示,我想这就是正确处理此类控件的巨大混乱的原因。此外,许多开发人员,尤其是刚刚接触 MVVM 的初学者,都认为代码隐藏违反了 MVVM。
由于某些不合理的原因,他们发现在视图模型组件中处理对话框视图的违规性较小。
由于其 API,
Window
看起来像一个简单的控件(事实上,它扩展了ContentControl
)。 但在底层,它与操作系统的低层架构挂钩。 要实现此目的需要大量非托管代码。 来自 MFC 等低级 C++ 框架的开发人员确切地知道幕后发生了什么。Window
和CommonDialog
类都是真正的混合体:它们是 WPF 框架的一部分,但为了表现得像或实际上成为本机操作系统窗口,它们也必须是低级操作系统基础结构的一部分。WPF
Window
以及CommonDialog
类基本上是复杂的低级操作系统 API 的包装器。 这就是为什么与纯框架控件相比,该控件有时会有一种奇怪的感觉(从开发人员的角度来看)。将
Window
作为简单的ContentControl
出售是非常具有欺骗性的。 但由于 WPF 是一个高级框架,所有低级细节在设计上都对 API 隐藏。我们必须接受这样的事实:我们必须仅使用 C# 来处理基于
Window
和CommonDialog
的控件。如果您愿意放弃本机外观和感觉以及一般操作系统集成以获得主题和任务栏等本机功能,您可以通过创建自定义对话框来改进处理,例如,通过扩展
Control
或Popup
,将相关属性公开为DependencyProperty
。 然后,您可以像平常一样设置数据绑定和 XAML 触发器来控制可见性。1.3 为什么MVVM?
如果没有复杂的设计模式或应用程序结构,开发人员将直接将数据库数据加载到表控件并将 UI 逻辑与业务逻辑混合。 在这种情况下,更改为不同的数据库会破坏 UI。 但更糟糕的是,更改 UI 将需要更改处理数据库的逻辑。 当改变逻辑时,你还需要改变相关的单元测试。
真正的应用程序是业务逻辑,而不是花哨的 GUI。
您想要为业务逻辑编写单元测试 - 而不必强制包含任何 UI。
您想要修改 UI 而不修改业务逻辑和单元测试。
MVVM 是一种解决此问题并允许将 UI 与业务逻辑(即数据与视图)解耦的模式。 它比相关的设计模式 MVC 和 MVP 更有效地做到这一点。
我们不希望 UI 渗透到应用程序的较低级别。 我们希望将数据与数据呈现分开,尤其是它们的渲染(数据视图)。 例如,我们希望处理数据库访问,而不必关心使用哪些库或控件来查看数据。 这就是我们选择MVVM的原因。 因此,我们不允许在视图以外的组件中实现 UI 逻辑。
1.4 为什么将 UI 逻辑从名为
ViewModel
的类移动到单独的类仍然违反 MVVM通过应用 MVVM,您可以有效地将应用程序构建为三个组件:模型、视图和视图模型。 理解这种划分或结构与类无关非常重要。 这是关于应用程序组件。
您可以遵循广泛传播的模式来命名或添加一个类后缀
ViewModel
,但您必须知道视图模型组件通常包含许多类,其中一些类没有以ViewModel
命名或后缀。 code> - 视图模型是一个抽象的应用程序组件。示例:
当您从名为
MainViewModel
的大类中提取功能(例如创建数据源集合)并将此功能移至名为ItemCreator
的新类时,则该类ItemCreator
在逻辑上仍然是视图模型组件的一部分。在类级别上,功能现在位于
MainViewModel
类之外。 在应用程序级别(架构级别),功能仍然位于同一组件中。您可以将此示例投影到经常提出的对话框服务上:将对话框逻辑从视图模型提取到名为
DialogService
的专用类不会将逻辑移出视图模型组件:视图模型 仍然取决于这个提取的功能。视图模型仍然参与 UI 逻辑,例如通过显式调用“服务”来控制对话框何时显示以及控制对话框类型本身(例如文件打开、文件夹选择、颜色选择器等)。 ).
这一切都需要了解 UI 的业务细节。 据了解,根据定义,该组件不属于视图模型组件。 当然,这样的知识引入了从视图模型组件到视图组件的耦合/依赖性。
职责根本不会改变,因为您命名了一个类
DialogService
而不是DialogViewModel
。因此,DialogService 是一种反模式,它隐藏了真正的问题:实现了依赖于 UI 并执行 UI 逻辑的视图模型类。
1.5 编写后台代码是否违反了MVVM设计模式?
MVVM 是一种设计模式,设计模式与每个定义库、框架以及语言或编译器无关。 因此,在谈论MVVM时,代码隐藏并不是一个话题。
代码隐藏文件绝对是编写 UI 代码的有效上下文。 它只是另一个包含 C# 代码的文件。 代码隐藏意味着“具有 .xaml.cs 扩展名且包含部分类的文件”。 它也是事件处理程序的唯一位置。 而且您不想远离事件。
为什么存在“代码隐藏中没有代码”这一口头禅?
对于刚接触 WPF、UWP 或 Xamarin 的人来说,比如来自 WinForms 等框架的熟练且经验丰富的开发人员,我们必须强调,使用 XAML 应该是编写 UI 代码的首选方式。 使用 C# 实现
Style
或DataTemplate
(例如在代码隐藏文件中)过于复杂,并且生成的代码非常难以阅读 => 很难理解=> 难以维护。XAML 非常适合此类任务。 视觉上冗长的标记风格完美地表达了 UI 的结构。 它在这方面做得比 C# 等要好得多。 尽管像 XAML 这样的标记语言可能会让人觉得不如某些语言或者不值得学习,但它绝对是实现 GUI 时的首选。 我们应该努力使用 XAML 编写尽可能多的 GUI 代码。
但这些考虑因素与MVVM设计模式完全无关。
代码隐藏只是一个编译器概念,由
partial
指令实现(在 C# 中)与 XAML 中的x:Class
指令配对。这就是为什么代码隐藏与任何设计模式无关。 这就是为什么 XAML 和 C# 都不能与任何设计模式有任何关系。
2 解决方案
就像OP正确得出的结论一样:
2.1 一些基本考虑因素
partial
或相关的臭名昭著的代码隐藏文件 (*.xaml.cs) 这样的东西。 代码隐藏的概念的存在是为了允许编译器将类的 XAML 部分与其 C# 部分合并。 合并后,两个文件都被视为一个类:这是一个纯粹的编译器概念。partial
是一种神奇的方法,可以使用 XAML 编写类代码(真正的编译器语言仍然是 C# 或任何其他 IL 兼容语言)。ICommand
是 .NET 库的一个接口,因此不是讨论 MVVM 时的主题。 相信每个操作都必须由视图模型中的ICommand
实现来触发是错误的。只要保持组件之间的单向依赖关系,事件仍然是一个非常有用的概念,符合MVVM。 总是强制使用
ICommand
而不是使用事件会导致不自然且有异味的代码,如 OP 提供的代码。ICommand
必须仅由视图模型类实现。 它也可以通过视图类来实现。事实上,视图通常实现
RoatedCommand
(或RoulatedUICommand
),它们都是ICommand
的实现,也可以用来触发显示来自例如Window
或任何其他控件的对话框。我们有数据绑定来允许 UI 与视图模型交换数据(从数据源的角度来看是匿名的)。 但由于数据绑定无法调用操作(至少在 WPF 中 - 例如,UWP 允许这样做),因此我们有
ICommand
和ICommandSource
来实现这一点。IFileDialogService
)永远无法解决MVVM相关问题。ViewModel
为后缀,也无法解决 MVVM 相关问题。2.2 结论
解决方案是显示类中的用户交互,该类是视图组件的一部分。
这意味着,这样的类必须是视图模型未知的类,因此视图模型无法调用该类。
此逻辑可以直接在代码隐藏文件或任何其他类中实现视图的(文件)。 实现可以是一个简单的帮助器类或更复杂的(附加的)行为。
要点是:对话框(即 UI 组件)必须由视图组件单独处理,因为这是唯一包含 UI 相关逻辑的组件。 由于视图模型不了解视图,因此它无法主动与视图进行通信。 仅允许被动通信(数据绑定、事件)。
我们始终可以使用视图模型引发的事件来实现特定的流程,视图可以观察到这些事件,以便采取诸如使用对话框与用户交互之类的操作。
存在使用视图模型优先方法的解决方案,该方法从一开始就不违反MVVM。 但仍然:设计不当的职责也可能使该解决方案变成反模式。
3 如何解决某些对话框请求的需要
大多数时候,我们可以通过修复应用程序的设计来消除在应用程序内显示对话框的需要。
由于对话框是一个支持与用户交互的 UI 概念,因此我们必须使用 UI 设计规则来评估对话框。
也许 UI 设计中最著名的设计规则是 Nielsen 和 Molich 在 90 年代提出的 10 条规则。
一条重要的规则是关于错误预防的:它指出
a) 我们必须防止任何类型的错误,尤其是与输入相关的错误,因为
b) 用户不喜欢他的工作效率被错误消息和对话框打断。
a) 表示:输入数据验证。 不允许无效数据进入业务逻辑。
b) 意味着:尽可能避免向用户显示对话框。 切勿从应用程序内显示对话框,并让用户显式触发对话框,例如单击鼠标(无意外中断)。
遵循这个简单的规则当然总是消除显示由视图模型触发的对话框的需要。
从用户的角度来看,应用程序是一个黑盒子:它输出数据、接受数据并处理输入数据。 如果我们控制数据输入以防止无效数据,我们就可以消除未定义或非法状态并确保数据完整性。 这意味着无需从应用程序内部向用户显示对话框。 仅那些由用户明确触发的。
例如,一个常见的场景是我们的模型需要将数据保存在文件中。 如果目标文件已经存在,我们要要求用户确认覆盖该文件。
遵循错误预防的规则,我们总是首先让用户选择文件:无论是源文件还是目标文件,总是由用户指定通过文件对话框显式选择该文件。 这意味着,用户还必须显式触发文件操作,例如通过单击“另存为”按钮。
这样,我们可以使用文件选择器或文件保存对话框来确保仅选择现有文件。 作为奖励,我们还消除了警告用户覆盖现有文件的需要。
采用这种方法,我们满足了a)“[...]防止任何类型的错误,特别是与输入相关的错误”和b)“[...]用户不喜欢被错误消息和对话框打断”。
更新
由于人们质疑您不需要视图模型来处理对话框视图,因此通过提出额外的“复杂”要求(例如数据验证)来证明他们的观点,我被迫提供更复杂的示例来解决这些更复杂的场景(OP 最初没有要求)。
4 示例
请参阅 GitHub:MVVM-Open-FileDialog-Example 了解完整示例实现包括一个(示例#4),它使用事件聚合器来使视图模型或模型能够以完全基于事件的方式将消息发布到 UI(例如,打开一个对话框 - 尽管任何此类消息传递方向都违反了 MVVM)。 实现本身符合 MVVM 标准,但从视图模型或模型向用户显示消息的意图是实际的 MVVM 违规。 GitHub 源代码中的示例 #4 包含更详细的解释。
4.1 概述
该场景是一个简单的输入表单,用于收集用户输入(例如相册名称),然后使用
OpenFileDialog
选择保存相册名称的目标文件。三个简单解决方案(加上 GitHub 示例中的一个额外解决方案):
解决方案 1:非常简单且基本的代码隐藏场景。
解决方案 2:在视图模型中使用数据验证。 为了使示例简单,省略了
INotifyDataErrorInfo
的实现。解决方案3:使用命令将对话框结果发送到视图模型。
解决方案 1
以下示例允许视图模型保持不知道任何 UI 组件或逻辑。
您甚至可以考虑将 FileStream 而不是文件路径传递给视图模型。 这样,您可以在创建流时直接在 UI 中处理任何错误,例如,根据需要显示一个对话框。
视图
MainWindow.xaml
MainWindow.xaml.cs
视图模型
MainViewModel.cs
解决方案2
更现实的解决方案是添加专用的
TextBox
作为输入字段,以通过复制和粘贴来收集目标文件路径。此
TextBox
绑定到视图模型类,理想情况下该类实现INotifyDataErrorInfo
来验证输入。附加按钮将打开可选的文件选择器视图,以允许用户选择浏览文件系统以选择目标。
最后,持久化操作由“另存为”按钮触发:
View
MainWindow.xaml
MainWindow.xaml.cs
View Model
MainViewModel.cs
解决方案 3
以下解决方案是第二种方案的更优雅的版本。 它使用
ICommandSource.CommandParameter
属性将对话框结果发送到视图模型(而不是上一个示例中使用的数据绑定)。使用绑定验证来验证可选用户输入(例如复制和粘贴)的验证:
View
MainWindow.xaml
FilePathValidationRule.cs
MainWindow.xaml.cs
模型
MainViewModel.cs
Long story short:
The solution is to show user interactions from a class, that is part of the view component.
This means, such a class must be a class that is unknown to the view model and therefore can't be invoked by the view model.
The solution of course can involve code-behind implementations as code-behind is not relevant when evaluating whether a solution complies with MVVM or not.
Beside answering the original question, this answer also tries to provide an alternative view on the general problem why controlling a UI component like a dialog from the view model violates the MVVM design pattern and why workarounds like a dialog service doesn't solve the problem.
1 MVVM and dialogs
1.1 Critique of common suggestions
Almost all answers are following the misconception that MVVM is a pattern, that targets class level dependencies and also requires empty code-behind files. But it's an architectural pattern, that tries to solve a different problem - on application/component level: keeping the business domain decoupled from the UI.
Most people (here on SO) agree that the view model should not handle dialogs, but then propose to move the UI related logic to a helper class (doesn't matter if it's called helper or service), which is still controlled by the view model.
This (especially the service version) is also known as dependency hiding. Such patterns are considered anti-patterns. Service Locator is the most infamous dependency hiding anti-pattern.
That is why I would call any pattern that involves extraction of the UI logic from the view model class to a separate class - in the view model component - an anti-pattern too. It does not solve the original problem: how to change the application structure or class design in order to remove the UI related responsibilities from a view model (or model) class and move it back to a view related class.
In other words: the critical logic remains being a part of the view model component.
For this reason, I do not recommend to implement solutions, like the accepted one, that involve a dialog service (whether it is hidden behind an interface or not). If you are concerned to write code that complies with the MVVM design pattern, then simply don't handle dialog views or UI directed messaging inside the view model.
Introducing an interface to decouple class level dependencies, for example an
IFileDialogService
interface, is called Dependency Inversion principle (the D in SOLID) and has nothing to do with MVVM. When it has no relevance in terms of MVVM, it can't solve an MVVM related problem. When room temperature does not have any relevance whether a structure is a four story building or a skyscraper, then changing the room temperature can never turn any building into a skyscraper.MVVM is not a synonym for Dependency Inversion.
MVVM is an architectural design pattern while Dependency Inversion is an OO language principle that has nothing to do with structuring an application (aka software architecture). It's not the interface (or the abstract type) that structures an application, but abstract objects or entities like components or modules e.g. Model - View - View Model. An interface can only help to "physically" decouple the modules. It doesn't remove component associations.
1.2 Why dialogs or handling
Window
in general feels so odd?We have to keep in mind that the dialog controls like
Microsoft.Win32.OpenFileDialog
are "low level" native Windows controls. They don't have the necessary API to smoothly integrate into an MVVM environment. Because of their true nature, they have some limitations the way they can integrate into a high-level framework like WPF. Dialogs or native window hosts in general, are a known "weakness" of all high-level frameworks in general.Dialogs are commonly based on the
Window
or the abstractCommonDialog
class. TheWindow
class is aContentControl
and therefore allows styles and templates to target the content.One big limitation is, that a
Window
must always be the root element. You can't add it as a child to the visual tree and e.g. show/launch it using triggers or host it in aDataTemplate
.In case of the
CommonDialog
, it can't be added to the visual tree, because it doesn't extendUIElement
.Therefore,
Window
orCommonDialog
based types must always be shown from code-behind, which I guess is the reason for the big confusion about handling this kind of controls properly.In addition, many developers, especially beginners that are new to MVVM, have the perception that code-behind violates MVVM.
For some irrational reason, they find it less violating to handle the dialog views in the view model component.
Due to its API, a
Window
looks like a simple control (in fact, it extendsContentControl
). But underneath, it hooks into to the low-level architecture of the OS. There is a lot of unmanaged code necessary to achieve this. Developers that are coming from low level C++ frameworks like MFC know exactly what's going on under the hoods.The
Window
andCommonDialog
class are both true hybrids: they are part of the WPF framework, but in order to behave like or actually to be a native OS window, they must be also part of the low level OS infrastructure.The WPF
Window
, as well as theCommonDialog
class, is basically a wrapper around the complex low-level OS API. That's why this controls have sometimes a strange feel (from the developer point of view), when compared to pure framework controls.That
Window
is sold as a simpleContentControl
is quite deceptive. But since WPF is a high-level framework, all low-level details are hidden from the API by design.We have to accept that we have to handle controls based on
Window
andCommonDialog
using C# only.If you are willing to waive the native look and feel and the general OS integration to get the native features like theming and task bar, you can improve the handling by creating a custom dialog e.g., by extending
Control
orPopup
, that exposes relevant properties asDependencyProperty
. You can then set up data bindings and XAML triggers to control the visibility, like you usually would.1.3 Why MVVM?
Without a sophisticated design pattern or application structure, developers would e.g., directly load database data to a table control and mix UI logic with business logic. In such a scenario, changing to a different database would break the UI. But even worse, changing the UI would require to change the logic that deals with the database. And when changing the logic, you would also need to change the related unit tests.
The real application is the business logic and not the fancy GUI.
You want to write unit tests for the business logic - without being forced to include any UI.
You want to modify the UI without modifying the business logic and unit tests.
MVVM is a pattern that solves this problems and allows to decouple the UI from the business logic i.e. data from views. It does this more efficiently than the related design patterns MVC and MVP.
We don't want to have the UI bleed into the lower levels of the application. We want to separate data from data presentation and especially their rendering (data views). For example, we want to handle database access without having to care which libraries or controls are used to view the data. That's why we choose MVVM. For this sake, we can't allow to implement UI logic in components other than the view.
1.4 Why moving UI logic from a class named
ViewModel
to a separate class still violates MVVMBy applying MVVM, you effectively structuring the application into three components: model, view and view model. It is very important to understand that this partitioning or structure is not about classes. It's about application components.
You may follow the widely spread pattern to name or suffix a class
ViewModel
, but you must know that the view model component usually contains many classes of which some are not named or suffixed withViewModel
- View Model is an abstract application component.Example:
when you extract functionality, like creating a data source collection, from a big class named
MainViewModel
and you move this functionality to a new class namedItemCreator
, then this classItemCreator
is logically still part of the view model component.On class level the functionality is now outside the
MainViewModel
class. On application level (architecture level), the functionality is still in the same component.You can project this example onto the often proposed dialog service: extracting the dialog logic from the view model to a dedicated class named
DialogService
doesn't move the logic outside the view model component: the view model still depends on this extracted functionality.The view model still participates in the UI logic e.g by explicitly invoking the "service" to control when dialog is shown and to control the dialog type itself (e.g., file open, folder select, color picker etc.).
This all requires knowledge of the UI's business details. Knowledge, that per definition does not belong into the view model component. Of course, such knowlegde introduces a coupling/dependency from the view model component to the view component.
Responsibilities simply don't change because you name a class
DialogService
instead of e.g.DialogViewModel
.The
DialogService
is therefore an anti-pattern, which hides the real problem: having implemented view model classes, that depend on UI and execute UI logic.1.5 Does writing code-behind violate the MVVM design pattern?
MVVM is a design pattern and design patterns are per definition library independent, framework independent and language or compiler independent. Therefore, code-behind is not a topic when talking about MVVM.
The code-behind file is absolutely a valid context to write UI code. It's just another file that contains C# code. Code-behind means "a file with a .xaml.cs extension that contains a partial class". It's also the only place for event handlers. And you don't want to stay away from events.
Why does the mantra "No code in code-behind" exist?
For people that are new to WPF, UWP or Xamarin, like those skilled and experienced developers coming from frameworks like WinForms, we have to stress that using XAML should be the preferred way to write UI code. Implementing a
Style
orDataTemplate
using C# (e.g. in the code-behind file) is too complicated and produces code that is very difficult to read => difficult to understand => difficult to maintain.XAML is just perfect for such tasks. The visually verbose markup style perfectly expresses the UI's structure. It does this far better, than for example C# could ever do. Despite markup languages like XAML may feel inferior to some or not worth learning it, it's definitely the first choice when implementing GUI. We should strive to write as much GUI code as possible using XAML.
But such considerations are absolutely irrelevant in terms of the MVVM design pattern.
Code-behind is simply a compiler concept, realized by the
partial
directive (in C#) paired with thex:Class
directive in XAML.That's why code-behind has nothing to do with any design pattern. That's why neither XAML nor C# can't have anything to do with any design pattern.
2 Solution
Like the OP correctly concludes:
2.1 Some fundamental considerations
OpenFileDialog
orUIElement
that breaks the MVVM pattern, but the implementation or reference of UI logic in the view model component or model component (although such a dependency can be a valuable hint).partial
or the related infamous code-behind file (*.xaml.cs). The concept of code-behind exists to allow the compiler to merge the XAML part of a class with its C# part. After that merge, both files are treated as a single class: it's a pure compiler concept.partial
is the magic that enables to write class code using XAML (the true compiler language is still C# or any other IL compliant language).ICommand
is an interface of the .NET library and therefore not a topic when talking about MVVM. It's wrong to believe that every action has to be triggered by anICommand
implementation in the view model.Events are still a very useful concept that conform with MVVM, as long as the unidirectional dependency between the components is maintained. Always forcing the use of
ICommand
instead of using events leads to unnatural and smelly code like the code presented by the OP.ICommand
must only be implemented by a view model class. It can be implemented by a view class too.In fact, views commonly implement
RoutedCommand
(orRoutedUICommand
), which both are implementions ofICommand
, and can also be used to trigger the display of a dialog from e.g., aWindow
or any other control.We have data binding to allow the UI to exchange data with the view model (anonymously, from the data source point of view). But since data binding can't invoke operations (at least in WPF - e.g., UWP allows this), we have
ICommand
andICommandSource
to realize this.IFileDialogService
) can never solve a MVVM related problem.ViewModel
, can't solve a MVVM related problem.2.2 Conclusion
The solution is to show user interactions from a class, that is part of the view component.
This means, such a class must be a class that is unknown to the view model and therefore can't be invoked by the view model.
This logic could be implemented directly in the code-behind file or inside any other class (file) of the view. The implementation can be a simple helper class or a more sophisticated (attached) behavior.
The point is: the dialog i.e. the UI component must be handled by the view component alone, as this is the only component that contains UI related logic. Since the view model does not have any knowledge of a view, it can't act actively to communicate with the view. Only passive communication is allowed (data binding, events).
We can always implement a certain flow using events raised by the view model that can be observed by the view in order to take actions like interacting with the user using a dialog.
There exist solutions using the view-model-first approach, which is does not violate MVVM in the first place. But still: badly designed responsibilities can turn this solution into an anti-pattern too.
3 How to fix the need for certain dialog requests
Most of the times, we can eliminate the need to show dialogs from within the application by fixing the application's design.
Since dialogs are a UI concept to enable interaction with the user, we must evaluate dialogs using UI design rules.
Maybe the most famous design rules for UI design are the 10 rules postulated by Nielsen and Molich in the 90's.
One important rule is about error prevention: it states that
a) we must prevent any kind of errors, especially input related, because
b) the user does not like his productivity to be interrupted by error messages and dialogs.
a) means: input data validation. Don't allow invalid data to enter the business logic.
b) means: avoid showing dialogs to the user, whenever possible. Never show a dialog from within the application and let the user trigger dialogs explicitly e.g., on mouse click (no unexpected interruption).
Following this simple rule certainly always eliminates the need to show a dialog triggered by the view model.
From the user's perspective, an application is a black box: it outputs data, accepts data and processes the input data. If we control the data input to guard against invalid data, we eliminate undefined or illegal states and ensure data integrity. This would mean that there is no need to ever show a dialog to the user from inside the application. Only those explicitly triggered by the user.
For example, a common scenario is that our model needs to persist data in a file. If the destination file already exists, we want to ask the user to confirm to overwrite this file.
Following the rule of error prevention, we always let the user pick files in the first place: whether it is a source file or a destination file, it's always the user who specifies this file by explicitly picking it via a file dialog. This means, the user must also explicitly trigger the file operation, for example by clicking on a "Save As" button.
This way, we can use a file picker or file save dialog to ensure only existing files are selected. As a bonus, we additionally eliminate the need to warn the user about overwriting existing files.
Following this approach, we have satisfied a) "[...]prevent any kind of errors, especially input related" and b) "[...]the user does not like to be interrupted by error messages and dialogs".
Update
Since people are questioning the fact that you don't need a view model to handle the dialog views, by coming up with extra "complicated" requirements like data validation to proof their point, I am forced to provide more complex examples to address these more complex scenarios (that were not initially requested by the OP).
4 Examples
See GitHub: MVVM-Open-FileDialog-Example for full example implementations including one (example #4) that uses an Event Aggregator to enable the view model or model to post messages to the UI (e.g. open a dialog - although any such messaging direction violates MVVM) in a fully event based manner. The implementation itself is MVVM compliant but the intent to display messages to a user from the view model or model is the actual MVVM violation. The example #4 from the GitHub sources contains a more detailed explanation.
4.1 Overview
The scenario is a simple input form to collect a user input like an album name and then use the
OpenFileDialog
to pick a destination file where the album name is saved to.Three simple solutions (plus one extra in the GitHub examples):
Solution 1: Very simple and basic code-behind scenario.
Solution 2: Uses data validation in the view model. To keep the example simple, the implementation of
INotifyDataErrorInfo
is omitted.Solution 3: Uses commands to send the dialog result to the view model.
Solution 1
The following example allows the view model to remain unaware of any UI components or logic.
You can even consider to pass a
FileStream
to the view model instead of the file path. This way, you can handle any errors, while creating the stream, directly in the UI e.g., by showing a dialog if needed.View
MainWindow.xaml
MainWindow.xaml.cs
View Model
MainViewModel.cs
Solution 2
A more realistic solution is to add a dedicated
TextBox
as input field to enable collection of the destination file path via copy&paste.This
TextBox
is bound to the view model class, which ideally implementsINotifyDataErrorInfo
to validate the inbput.An additional button will open the optional file picker view to allow the user to alternatively browse the file system to pick a destination.
Finally, the persistence operation is triggered by a "Save As" button:
View
MainWindow.xaml
MainWindow.xaml.cs
View Model
MainViewModel.cs
Solution 3
The following solution is a more elegant version of the second scenario. It uses the
ICommandSource.CommandParameter
property to send the dialog result to the view model (instead of the data binding used in the previous example).The validation of the optional user input (e.g. copy&paste) is validated using binding validation:
View
MainWindow.xaml
FilePathValidationRule.cs
MainWindow.xaml.cs
View Model
MainViewModel.cs
在这里最好的办法就是使用服务。
服务只是一个类,您可以从服务的中央存储库(通常是 IOC 容器)访问该类。 然后该服务会实现您需要的功能,例如 OpenFileDialog。
因此,假设您在 Unity 容器中有一个
IFileDialogService
,您可以这样做...The best thing to do here is use a service.
A service is just a class that you access from a central repository of services, often an IOC container. The service then implements what you need like the OpenFileDialog.
So, assuming you have an
IFileDialogService
in a Unity container, you could do...我本来想对其中一个答案发表评论,但可惜的是,我的声誉不够高,无法这样做。
诸如 OpenFileDialog() 之类的调用违反了 MVVM 模式,因为它暗示了视图模型中的视图(对话框)。 视图模型可以调用类似 GetFileName() 的方法(也就是说,如果简单绑定还不够),但它不应该关心如何获取文件名。
I would have liked to comment on one of the answers, but alas, my reputation is not high enough to do so.
Having a call such as OpenFileDialog() violates the MVVM pattern because it implies a view (dialog) in the view model. The view model can call something like GetFileName() (that is, if simple binding is not sufficient), but it should not care how the file name is obtained.
我使用一个服务,例如,我可以将其传递到我的 viewModel 的构造函数中或通过依赖项注入进行解析。
例如
和一个实现它的类,在底层使用 OpenFileDialog。 在 viewModel 中,我只使用接口,因此可以根据需要模拟/替换它。
I use a service which i for example can pass into the constructor of my viewModel or resolve via dependency injection.
e.g.
and a class implementing it, using OpenFileDialog under the hood. In the viewModel, i only use the interface and thus can mock/replace it if needed.
ViewModel 不应该打开对话框,甚至不应该知道它们的存在。 如果 VM 位于单独的 DLL 中,则项目不应引用PresentationFramework。
我喜欢在常见对话框的视图中使用辅助类。
帮助器类公开窗口在 XAML 中绑定到的命令(而不是事件)。 这意味着在视图中使用 RelayCommand。 辅助类是一个 DepencyObject,因此它可以绑定到视图模型。
辅助类需要对 ViewModel 实例的引用。 请参阅资源字典。 构造完成后,将设置 ViewModel 属性(在 XAML 的同一行中)。 这是当帮助器类上的 FileName 属性绑定到视图模型上的 FileName 属性时。
The ViewModel should not open dialogs or even know of their existence. If the VM is housed in a separate DLL, the project should not have a reference to PresentationFramework.
I like to use a helper class in the view for common dialogs.
The helper class exposes a command (not an event) which the window binds to in XAML. This implies the use of RelayCommand within the view. The helper class is a DepencyObject so it can bind to the view model.
The helper class needs a reference to the ViewModel instance. See the resource dictionary. Just after construction, the ViewModel property is set (in the same line of XAML). This is when the FileName property on the helper class is bound to the FileName property on the view model.
我已经用这种方式解决了这个问题:
ViewModel
下面的代码中未实现 CommandImpl。
视图模型:
视图:
XAML:
I have solved it for me this way:
ViewModel
CommandImpl is not implemented in code below.
ViewModel:
View:
XAML:
拥有服务就像从视图模型中打开一个视图。
我看到了一个依赖属性,在更改该属性时,我打开 FileDialog 并读取路径,更新该属性,从而更新虚拟机的绑定属性
Having a service is like opening up a view from viewmodel.
I have a Dependency property in view, and on the chnage of the property, I open up FileDialog and read the path, update the property and consequently the bound property of the VM
该问题询问“专家”如何在利用 MVVM 设计模式的同时处理 WPF 中打开的文件对话框。 我不知道我是否是“专家”,但我是专业人士,所以我希望这很重要。 :)
由于这个问题特定于打开文件,我只会演示这一点,但我有另一个答案,将此解决方案扩展到任何可模板化的对话框内容。 此解决方案不涉及特定于应用程序的代码隐藏(即,用于特定于应用程序的视图的空
.xaml.cs
文件)。 它还不涉及视图模型对视图类型(注入或其他)的依赖。我将从应用程序端的最终结果开始。
视图:
视图模型:
这是整个应用程序特定的代码。 我会在这里暂停说,如果您认为这不是有效的 MVVM 方法,那么您最好停止阅读。 尽管我认为我的说法是有充分依据的,但我不想就这一点进行辩论。 客观地说,它将抽象的、平台无关的逻辑与 GUI 细节分开,并且它允许在 XAML 中 100% 声明性定义视图(即无代码隐藏)。 而且它具有高度可扩展性。 就我个人而言,我认为这些都是值得努力的事情,并且我已经就这个主题撰写了大量文章。 不过,如果这些事情与您无关,那么请务必遵循上面得票最高的答案中的方法。 我不会被冒犯(尽管你的开发预算可能会被冒犯)。
但如果您确实希望能够以这种方式打开文件对话框和其他对话框,请继续阅读。
我们确实需要做一些脚手架来完成这项工作,但好处是所有这些都可以在其他应用程序中重用(大多数甚至在其他平台上也是如此)。 从与平台无关的部分开始:
最后是 WPF 特定的部分:
如果我们想要做的只是显示一个文件打开对话框,那么我们就完成了。 当然,您可以轻松地将其扩展到文件保存、文件夹选择,甚至消息框。 您可以进一步扩展 FileOpenDialogViewModel 来接受过滤器参数或初始建议等。
您可能会问,“这真的比代码隐藏更好吗”? (如果你了解我,你就会知道我是反对代码隐藏的人)。 答案是肯定的,因为并非所有代码隐藏都是一样的。
虽然
DialogManager
类与DialogViewModel
类混合在一起,但连接不是在应用程序级别发生的 - 它发生在我称之为服务的地方 > 或图书馆级别。 无论我们如何称呼它,重点是它是可重复使用的且适应性强。 如果我们想将其移植到 WinUI、MAUI,甚至 Web(通过 Blazor),我们所要做的就是重写一个新的特定于平台的DialogManager
就可以了。 我们想要避免的并不是所有 C# GUI 代码 - 当然这是荒谬的。 我们希望避免定制的、特定于应用程序的代码隐藏,这种代码隐藏不必要地将应用程序逻辑与 GUI 细节混合在一起,并且不提供可重用性潜力。想象一下,如果我们采纳其他人的建议,只使用常规的代码隐藏,那么最终会出现 25 个使用文件打开对话框的地方。 然后想象一下,在某个时候我们意识到我们不想使用 OpenFileDialog,或者必须一起迁移到另一个平台。 你的假期就这样过去了。
因此,当您问“专家”(或至少是专业人士)会做什么时? - 他们会做这样的事情。 能够扩展、最小化成本并最大化价值的东西。
The question asks how an "expert" would handle an open file dialog in WPF while utilizing the MVVM design pattern. I don't know if I'm an "expert", but I am a professional, so I hope that counts. :)
Since this question is specific to open file I'll only demonstrate that, but I have another answer that extends this solution to any templatable dialog content. This solution involves no application-specific code-behind (i.e. an empty
.xaml.cs
file for the application-specific view). It also involves no dependency by the view model on view types - injected or otherwise.I'll start with what the end-result would look like on the application side.
View:
View Model:
That's the entirety of application-specific code. I'll pause at this point to say, if you don't think this is a valid MVVM approach, then you might as well stop reading. Though I think I'm on pretty solid ground saying it is, I have no desire to get into a debate on that point. Objectively speaking, it separates the abstract, platform-independent logic from the GUI specifics, and it allows a 100% declaratively defined view in XAML (i.e. no code-behind). And it's highly scalable. Personally, I think those are worthy things to strive for and I've written extensively on the topic. If those things don't concern you, though, then by all means follow the approach in the highest-voted answer above. I won't be offended (though your development budget might be).
But if you do want to be able to open file dialogs and others in this manner, then read on.
We do need to do a bit of scaffolding to make this work, but the upside is that all of it will be reusable in other applications (and most if it even in other platforms). Starting with the platform-independent parts:
And finally the WPF-specific portion:
If all we want to do is display a file open dialog, we're done. Of course you can easily extend this to file save, folder select, and even message boxes. You could further extend
FileOpenDialogViewModel
to accept parameters for filters, or an initial suggestion, etc.You may be asking, "is this really any better than code-behind"? (And if you know me, you know I'm about as anti-code-behind as they come). The answer is yes, because not all code-behind is created equal.
While the
DialogManager
class is comingled withDialogViewModel
class, the connection is not happening at the application level - it's happening at what I'd call the service or library level. Whatever we call it, the point is that it's resuable and highly adaptable. If we wanted to port this to WinUI, or MAUI, or even the web (through Blazor), all we'd have to do is rewrite a new platform-specificDialogManager
and we'd be done. What we want to avoid is not all C# GUI code - of course that's ridiculous. We want to avoid bespoke, application-specific code-behind that comingles application logic with GUI specifics unnecessarily and offers no reusability potential.Imagine if we went with others' suggestion to just use regular code-behind, and then wound up with 25 places where a file open dialog was used. And then imagine at some point we realized we didn't want to use
OpenFileDialog
, or had to move to another platform all together. There goes your vacation.So when you ask, what would an "expert" (or at least a professional) do? - they would do something like this. Something that scales, minimizes cost, and maximizes value.