Jos*_*ose 45 wpf openfiledialog mvvm
好的,我真的想知道专家MVVM开发人员如何处理WPF中的openfile对话框.
我真的不想在我的ViewModel中执行此操作(其中'Browse'通过DelegateCommand引用)
void Browse(object param)
{
//Add code here
OpenFileDialog d = new OpenFileDialog();
if (d.ShowDialog() == true)
{
//Do stuff
}
}
Run Code Online (Sandbox Code Playgroud)
因为我认为这违背了MVVM方法论.
我该怎么办?
Cam*_*and 36
这里最好的事情是使用服务.
服务只是您从中央服务存储库(通常是IOC容器)访问的类.然后,该服务实现您需要的功能,如OpenFileDialog.
所以,假设你有IFileDialogService一个Unity容器,你可以做......
void Browse(object param)
{
var fileDialogService = container.Resolve<IFileDialogService>();
string path = fileDialogService.OpenFileDialog();
if (!string.IsNullOrEmpty(path))
{
//Do stuff
}
}
Run Code Online (Sandbox Code Playgroud)
Bio*_*ode 11
长话短说:
解决方案是显示OpenFileDialog来自一个类,即视图组件的一部分。
这意味着,这样的类必须是视图模型未知的类,因此不能被视图模型调用。
解决方案当然可能涉及代码隐藏实现,因为在评估解决方案是否符合MVVM时,代码隐藏并不相关。
除了回答原始问题之外,这个答案还试图提供一个关于一般问题的替代视图,为什么从视图模型控制像对话框这样的 UI 组件违反了MVVM设计模式,以及为什么像对话服务这样的变通方法不能解决问题。
几乎所有的答案都遵循这样的误解,即MVVM是一种模式,它针对类级别的依赖关系,并且还需要空的代码隐藏文件。但它是一种架构模式,它试图解决一个不同的问题 - 在应用程序/组件级别:保持业务领域与 UI 分离。
大多数人(在 SO 上)都同意视图模型不应该处理对话框,但随后建议将 UI 相关逻辑移至助手类(无论它是称为助手还是服务都无关紧要),该类仍由视图控制模型。
这(尤其是服务版本)也称为依赖隐藏. 许多模式都这样做。这种模式被认为是反模式。Service Locator 是最著名的依赖隐藏反模式。
这就是为什么我将任何涉及从视图模型类中提取 UI 逻辑的模式称为反模式的原因。它没有解决最初的问题:如何更改应用程序结构或类设计,以便从视图模型(或模型)类中移除 UI 相关职责并将其移回视图相关类。
换句话说:关键逻辑仍然是视图模型组件的一部分。
出于这个原因,我不建议实施解决方案,如已接受的解决方案,涉及对话服务(无论它是否隐藏在接口后面)。如果您希望编写符合MVVM设计模式的代码,那么就不要在视图模型中处理对话框视图。
引入一个接口来解耦类级别的依赖称为依赖倒置原理(SOLID 中的 D),与MVVM无关。当它与MVVM没有相关性时,它无法解决与MVVM相关的问题。当室温与结构是四层楼还是摩天大楼没有任何关系时,改变室温永远无法将此类建筑变成摩天大楼。否则MVVM将意味着:使用Dependency Inversion来实现初始目标或MVVM是Dependency Inversion的别名。
MVVM是一种架构模式,而依赖倒置是一种 OO 语言原则,与构建应用程序(又名软件架构)无关。构建应用程序的不是接口(或抽象类型),而是抽象对象或实体,如组件或模块,例如模型 - 视图 - 视图模型。接口只能帮助“物理”解耦组件或模块。它不会删除组件关联。
Window一般感觉如此奇怪?我们必须记住,对话框控件Microsoft.Win32.OpenFileDialog是“低级”本机Windows 控件。他们没有必要的 API 来顺利地将它们集成到MVVM环境中。由于它们的真实性质,它们在集成到 WPF 等高级框架的方式上存在一些限制。对话框通常是框架或高级框架的已知“弱点”。尤其是在谈论本机操作系统对话框时。
对话框通常基于Window或 抽象CommonDialog类。该Window班是一个ContentControl,因此允许样式和模板为目标的内容。
一个很大的限制是 aWindow必须始终是根元素。您不能将其作为直接子项添加到可视化树中,例如使用触发器显示/启动它。
在 的情况下CommonDialog,它不能添加到可视化树中,因为它没有扩展UIElement。
因此Window或CommonDialog基于类型必须始终从代码隐藏中显示,我想这就是正确处理此类控件时出现大混乱的原因。此外,许多开发人员,尤其是刚接触 MVVM 的初学者,都认为代码隐藏违反了MVVM。
由于一些不合理的原因,他们发现在视图模型组件中处理对话框视图不那么违反。
由于它的 API,aWindow看起来像一个简单的控件。但在它下面是一个与操作系统底层挂钩的控件。有很多非托管代码需要实现这一点。来自 MFC 等低级 C++ 框架的开发人员确切地知道幕后发生了什么。
在Window和CommonDialog类都是真杂种:他们是WPF框架的一部分,但为了表现得像或者实际上是本地操作系统窗口,他们必须也低级别OS基础设施的一部分。
WPFWindow以及CommonDialog类基本上是复杂的低级 OS API 的包装器。这就是为什么当与普通和纯框架控件相比时,这些控件有时会有一种奇怪的感觉(从开发人员的角度来看)。简单地
说Window就是卖相ContentControl是相当具有欺骗性的。但由于 WPF 是一个高级框架,所有低级细节都被设计为对 API 隐藏。
我们必须接受我们必须处理基于Window和CommonDialog仅使用 C# 并且代码隐藏根本不违反任何设计模式。
如果您愿意放弃本机外观,您可以通过创建一个公开相关属性的自定义对话框来改进处理DependencyProperty,然后在代码隐藏中设置数据绑定。由于CommonDialog不扩展DependencyObject,您只能扩展Window或扩展Control(当然Control没有常见和预期的本机窗口功能,如操作系统集成,例如任务栏等)。
如果没有复杂的设计模式或应用程序结构,开发人员将例如通过将 UI 对象引用与业务对象引用混合,直接将数据库数据加载到 UI 的表控件。在这种情况下,更改为不同的数据库会破坏 UI。但更糟糕的是,更改 UI 需要更改处理数据库的逻辑。在更改逻辑时,您还需要更改相关的单元测试。
真正的应用程序是业务逻辑而不是花哨的 GUI。
您想为业务逻辑编写单元测试 - 不必为了能够测试业务逻辑而被迫测试 UI。
您希望在不修改业务逻辑的情况下修改 UI。
MVVM是一种解决问题并允许将 UI 与业务逻辑分离的模式。它比相关的设计模式MVC和MVP更有效地做到这一点。
我们不希望 UI 渗入应用程序的较低级别。我们希望将数据与数据表示分离,尤其是它们的呈现(数据视图)。例如,我们希望处理数据库访问,而不必关心使用哪些库或控件来查看数据。这就是我们首先选择MVVM的原因。为此,我们不允许在视图以外的组件中实现 UI 逻辑。
ViewModel到单独的类仍然违反MVVM要应用MVVM,您将应用程序划分为三个组件:模型、视图和视图模型。这不是关于类,而是应用程序组件或智能应用程序结构。
您可以遵循广泛传播的模式来命名或后缀 class ViewModel,但您必须知道视图模型组件通常包含更多类,其中一些未命名或后缀ViewModel- 它是一个组件。
示例:当您从名为 的大类中提取功能(例如创建数据源集合)MainViewModel并将此功能移动到名为 的类时ItemCreator,该类ItemCreator也是视图模型的一部分(假设提取的功能位于正确的组件中)前)。
您可以将此示例投影到经常提出的对话服务上:从视图模型中提取对话逻辑到一个名为的专用类DialogService不会将逻辑移到视图模型组件之外:视图模型仍然依赖于这个提取的功能。视图模型仍然参与UI 逻辑,例如通过控制对话框显示或隐藏的时刻以及通过控制对话框类型本身(例如,文件打开、文件夹选择、颜色选择器等),这些都需要 UI 的业务知识细节。
职责之类的东西不会简单地改变,因为您将这个新类命名为 aDialogService而不是 eg DialogViewModel。
的DialogService因此是抗图案,其隐藏了真正的问题:具有实现视图模型类,依赖于UI和UI执行逻辑。
由于MVVM是一种设计模式,并且设计模式与每个定义库无关,与框架无关,与语言或编译器无关。因此,在谈论MVVM时,代码隐藏不是一个话题。
代码隐藏文件绝对是编写 UI 代码的有效上下文。它只是另一个包含 C# 代码的文件。代码隐藏意味着“具有.xaml.cs 扩展名的文件”。
除了事件处理程序和名称范围相关的引用之外,将代码写入此代码隐藏文件还是单独的类文件实际上并不重要。由于代码隐藏.xaml.cs文件是.xaml的一部分,它允许引用为保存命名的 XAML 元素而创建的字段。
为什么存在“代码隐藏中没有代码”的口头禅?对于刚接触 WPF、UWP 或 Xamarin 的人,比如那些来自 WinForms 等框架的熟练和经验丰富的开发人员,我们必须强调,使用 XAML 应该是编写 UI 代码的首选方式。实现Style或DataTemplate使用C#(例如在代码隐藏文件中)过于复杂,并且产生的代码很难阅读=> 难以理解=> 难以维护。
XAML 非常适合此类任务。视觉上冗长的标记样式完美地表达了 UI 的结构,远胜于 C#。尽管像 XAML 这样的标记语言可能感觉不如某些语言或不值得学习,但它绝对是实现 GUI 时的首选。
但这些考虑与MVVM设计模式完全无关。
代码隐藏只是一个 C# 语言编译器概念,由partial指令实现。这就是代码隐藏与任何设计模式无关的原因。这就是 XAML 和 C# 都与设计模式无关的原因。
就像 OP 正确得出的结论:
“我真的不想在我的 ViewModel 中执行此操作 [打开文件选择器对话框](其中 'Browse' 是通过 DelegateCommand 引用的)。因为我认为这违背了MVVM方法。
一些基本考虑:
OpenFileDialog或UIElement压垮MVVM图案,但实现或在视图模型部件或模型组件UI逻辑的参考(虽然这样的依赖关系可以是有价值的提示)。partial或相关的臭名昭著的代码隐藏文件。代码隐藏的概念允许编译器将类的 XAML 部分与其 C# 部分合并。合并之后,两个文件都被视为一个类:这是一个纯粹的编译器概念。partial是使用 XAML 编写类代码的魔法(真正的编译器语言仍然是 C# 或任何其他符合 IL 的语言)。ICommand是 .NET 库的一个接口,因此在谈论MVVM时不是一个话题。认为每个动作都必须由ICommand视图模型中的实现触发是错误的。ICommand而不是使用事件会导致像 OP 提供的代码那样不自然和发臭的代码。ICommand只能由视图模型类实现。它也可以通过视图类来实现。RoutedCommand(或RoutedUICommand),它们都是 的实现ICommand,也可以用来触发对话框的显示,例如,aWindow或任何其他控件。ICommand并ICommandSource意识到这一点。ViewModel,也无法解决MVVM相关问题。解决方案是显示OpenFileDialog来自一个类,即视图组件的一部分。
这意味着,这样的类必须是视图模型未知的类,因此不能被视图模型调用。
此逻辑可以直接在代码隐藏文件中或在任何其他类(文件)中实现。实现可以是一个简单的辅助类或更复杂的(附加的)行为。重点是:对话框即 UI 组件必须由视图组件触发,因为这是唯一包含 UI 相关逻辑的组件。由于视图模型对视图没有任何了解,因此它无法主动与视图进行通信。只允许被动通信(数据绑定)。
由于人们质疑您不需要视图模型来处理对话框视图的事实,因此通过提出额外的“复杂”要求(如数据验证)来证明他们的观点,我被迫提供更复杂的示例来解决这些问题复杂的场景(最初不是由 OP 请求的)。
存在使用视图模型优先方法的解决方案,与“对话框服务”相比,这种方法更胜一筹,但仍然没有解决设计不当的职责或组件的问题,因为所有对话框视图,即 UI 流仍然由视图模型组件。
该场景是一个简单的输入表单,用于收集用户输入(如专辑名称),然后使用OpenFileDialog选择保存专辑名称的目标文件夹。
三个简单的解决方案:
INotifyDataErrorInfo。属性设置方法只调用一个Validate()成员。ICommand和ICommandSource.CommandParameter将对话结果发送到视图模型并执行操作的解决方案。下面的例子提供了一种简单和直观的解决方案,以显示OpenFileDialog在一MVVM兼容的方式。
该解决方案允许视图模型保持不知道任何 UI 组件或逻辑。它所做的只是通过数据绑定向/从视图公开/接收数据,并通过SaveAlbumName()向视图公开公共方法将此数据传递给模型以进行数据持久化:
主窗口.xaml
<Window>
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<StackPanel>
<TextBox Text="{Binding AlbumName}" />
<Button Click="AppendAlbumNameToFile_OnClick" />
</StackPanel>
</Window>
Run Code Online (Sandbox Code Playgroud)
主窗口.xaml.cs
partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void AppendAlbumNameToFile_OnClick(object sender, EventArgs e)
{
var dialog = new OpenFileDialog();
if (dialog.ShowDialog() == true)
{
string destinationFilePath = dialog.FileName;
(this.DataContext as MainViewModel)?.SaveAlbumName(destinationFilePath);
}
}
}
Run Code Online (Sandbox Code Playgroud)
主视图模型.cs
class MainViewModel : INotifyPropertyChanged
{
private string albumName;
public string AlbumName
{
get => this.albumName;
set
{
this.albumName = value;
OnPropertyChanged();
}
}
// A model class that is responsible to persist and load data
private DataRepository DataRepository { get; }
// Default constructor
public MainViewModel() => this.DataRepository = new DataRepository();
public void SaveAlbumName(string destinationFilePath)
{
// Use a aggregated/composed model class to persist the data
this.DataRepository.SaveData(this.AlbumName, destinationFilePath);
}
}
Run Code Online (Sandbox Code Playgroud)
更现实的解决方案是通过添加专用TextBox作为输入字段来收集目标文件路径来交替输入流。
一个按钮将打开可选的文件选择器视图,以允许用户交替浏览文件系统以查找目标路径。
浏览器结果被分配给TextBox绑定到视图模型类的 。通过这种方式可以验证文件路径,例如,通过实现INotifyDataErrorInfo或使用绑定验证。
最终使用ICommand注册的“保存”按钮调用视图模型操作:
主窗口.xaml
<Window>
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<StackPanel>
<TextBox Text="{Binding AlbumName}" />
<TextBox x:Name="FilePathTextBox" Text="{Binding TextValue, ValidatesOnNotifyDataErrors=True}" />
<Button Content="Browse" Click="PickFile_OnClick" />
<Button Content="Save" Command="{Binding SaveAlbumNameCommand}" />
</StackPanel>
</Window>
Run Code Online (Sandbox Code Playgroud)
主窗口.xaml.cs
partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void PickFile_OnClick(object sender, EventArgs e)
{
var dialog = new OpenFileDialog();
if (dialog.ShowDialog() == true)
{
this.FilePathTextBox.Text = dialog.FileName;
// Since setting the property bypasses the data binding we must explicitly update it
this.FilePathTextBox.GetBindingExpression(TextBox.TextProperty).UpdateSource();
}
}
}
Run Code Online (Sandbox Code Playgroud)
主视图模型.cs
class MainViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
private string albumName;
public string AlbumName
{
get => this.albumName;
set
{
this.albumName = value;
OnPropertyChanged();
}
}
private string destinationPath;
public string DestinationPath
{
get => this.destinationPath;
set
{
this.destinationPath= value;
OnPropertyChanged();
ValidateDestinationFilePath();
}
}
public ICommand SaveAlbumNameCommand => new RelayCommand(
commandParameter =>
{
ExecuteSaveAlbumName(this.TextValue);
},
commandParameter => true);
// A model class that is responsible to persist and load data
private DataRepository DataRepository { get; }
// Default constructor
public MainViewModel() => this.DataRepository = new DataRepository();
private void ExecuteSaveAlbumName(string destinationFilePath)
{
// Use a aggregated/composed model class to persist the data
this.DataRepository.SaveData(this.AlbumName, destinationFilePath);
}
}
Run Code Online (Sandbox Code Playgroud)
以下解决方案使用该ICommandSource.CommandParameter属性将对话框结果发送到视图模型并关联ICommandSource.Command以触发操作:
主窗口.xaml
<Window x:Name="Window">
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<StackPanel>
<TextBox Text="{Binding AlbumName}" />
<TextBox x:Name="FilePathTextBox" Text="{Binding TextValue, ValidatesOnNotifyDataErrors=True}" />
<Button Content="Browse" Click="PickFile_OnClick" />
<Button Content="Save"
CommandParameter="{Binding ElementName=Window, Path=DestinationPath}"
Command="{Binding SaveAlbumNameCommand}" />
</StackPanel>
</Window>
Run Code Online (Sandbox Code Playgroud)
主窗口.xaml.cs
partial class MainWindow : Window
{
public static readonly DependencyProperty DestinationPathProperty = DependencyProperty.Register(
"DestinationPath",
typeof(string),
typeof(MainWindow),
new PropertyMetadata(default(string)));
public string DestinationPath
{
get => (string) GetValue(MainWindow.DestinationPathProperty);
set => SetValue(MainWindow.DestinationPathProperty, value);
}
public MainWindow()
{
InitializeComponent();
}
private void PickFile_OnClick(object sender, EventArgs e)
{
var dialog = new OpenFileDialog();
if (dialog.ShowDialog() == true)
{
this.DestinationPath = dialog.FileName;
}
}
}
Run Code Online (Sandbox Code Playgroud)
主视图模型.cs
class MainViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
private string albumName;
public string AlbumName
{
get => this.albumName;
set
{
this.albumName = value;
OnPropertyChanged();
}
}
public ICommand SaveAlbumNameCommand => new RelayCommand(
commandParameter =>
{
ExecuteSaveAlbumName(commandParameter as string);
},
commandParameter => true);
// A model class that is responsible to persist and load data
private DataRepository DataRepository { get; }
// Default constructor
public MainViewModel() => this.DataRepository = new DataRepository();
private void ExecuteSaveAlbumName(string destinationFilePath)
{
// Use a aggregated/composed model class to persist the data
this.DataRepository.SaveData(this.AlbumName, destinationFilePath);
}
}
Run Code Online (Sandbox Code Playgroud)
我本来希望对其中一个答案发表评论,但唉,我的声誉还不够高.
进行诸如OpenFileDialog()之类的调用会违反MVVM模式,因为它意味着视图模型中的视图(对话框).视图模型可以调用类似GetFileName()的东西(也就是说,如果简单绑定不够),但它不应该关心如何获取文件名.
我使用一个服务,例如我可以传递给我的viewModel的构造函数或通过依赖注入解析.例如
public interface IOpenFileService
{
string FileName { get; }
bool OpenFileDialog()
}
Run Code Online (Sandbox Code Playgroud)
以及实现它的类,使用OpenFileDialog.在viewModel中,我只使用接口,因此可以根据需要进行模拟/替换.
ViewModel不应该打开对话框,甚至不知道它们的存在.如果VM位于单独的DLL中,则项目不应引用PresentationFramework.
我喜欢在视图中使用辅助类来进行常见对话.
辅助类公开窗口在XAML中绑定的命令(不是事件).这意味着在视图中使用RelayCommand.辅助类是DepencyObject,因此它可以绑定到视图模型.
class DialogHelper : DependencyObject
{
public ViewModel ViewModel
{
get { return (ViewModel)GetValue(ViewModelProperty); }
set { SetValue(ViewModelProperty, value); }
}
public static readonly DependencyProperty ViewModelProperty =
DependencyProperty.Register("ViewModel", typeof(ViewModel), typeof(DialogHelper),
new UIPropertyMetadata(new PropertyChangedCallback(ViewModelProperty_Changed)));
private static void ViewModelProperty_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (ViewModelProperty != null)
{
Binding myBinding = new Binding("FileName");
myBinding.Source = e.NewValue;
myBinding.Mode = BindingMode.OneWayToSource;
BindingOperations.SetBinding(d, FileNameProperty, myBinding);
}
}
private string FileName
{
get { return (string)GetValue(FileNameProperty); }
set { SetValue(FileNameProperty, value); }
}
private static readonly DependencyProperty FileNameProperty =
DependencyProperty.Register("FileName", typeof(string), typeof(DialogHelper),
new UIPropertyMetadata(new PropertyChangedCallback(FileNameProperty_Changed)));
private static void FileNameProperty_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Debug.WriteLine("DialogHelper.FileName = {0}", e.NewValue);
}
public ICommand OpenFile { get; private set; }
public DialogHelper()
{
OpenFile = new RelayCommand(OpenFileAction);
}
private void OpenFileAction(object obj)
{
OpenFileDialog dlg = new OpenFileDialog();
if (dlg.ShowDialog() == true)
{
FileName = dlg.FileName;
}
}
}
Run Code Online (Sandbox Code Playgroud)
辅助类需要对ViewModel实例的引用.请参阅资源字典.构建完成后,将设置ViewModel属性(在XAML的同一行中).这是辅助类上的FileName属性绑定到视图模型上的FileName属性的时候.
<Window x:Class="DialogExperiment.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DialogExperiment"
xmlns:vm="clr-namespace:DialogExperimentVM;assembly=DialogExperimentVM"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<vm:ViewModel x:Key="viewModel" />
<local:DialogHelper x:Key="helper" ViewModel="{StaticResource viewModel}"/>
</Window.Resources>
<DockPanel DataContext="{StaticResource viewModel}">
<Menu DockPanel.Dock="Top">
<MenuItem Header="File">
<MenuItem Header="Open" Command="{Binding Source={StaticResource helper}, Path=OpenFile}" />
</MenuItem>
</Menu>
</DockPanel>
</Window>
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
41654 次 |
| 最近记录: |