一个用于原型设计和 MVVM 练习的玩具标签式文件浏览器
本文描述了一个具有最小功能、仅使用基本MVVM技术和一些附加属性的选项卡式文件浏览器。在第一篇文章中,我描述了一个MVVM选项卡式导航树,在本文中,我添加了一个选项卡式文件夹面板。
目录
- 引言
- 背景
- 关于附加属性的简短说明
- 关于设计和实现过程的简短说明
- 模型:添加FolderPlane、FolderPlaneItem、SavedFolderTabsItem
- ViewModel
- 属性
- 示例setter属性:SelectedPath
- ICommands列表
- 视图
- MainViewWindow
- FolderPlaneView
- TabsFolderPlanesView
- SavedFolderTabsView
- FileManagementButtonsView
- 如何制作一个更成熟的文件浏览器
- 关注点
- 许可证
- 历史
引言
这是系列两篇文章中的第二篇。在我的第一篇文章使用MVVM选项卡式TreeView玩文件浏览器中,本文的先决条件,我描述了一个简单的选项卡式TreeView,在这第二篇文章中,我将添加一个选项卡式文件夹面板。文件管理是暂时的。最终程序可以作为原型来测试选项卡如何用于文件浏览器中的某些任务。这个示例全局展示了MVVM的一个优点,即在具有一些交互的桌面应用程序中实现关注点分离。
我的主要目的不是编写一个功能齐全的文件浏览器,而是更多地了解WPF(我读了一些书,但仍处于复制/粘贴阶段),并获得我的第一次MVVM编程实践经验。
最终程序可以作为原型,初步了解选项卡如何在文件浏览器中使用以及它们有多有用。
在第一篇文章中,我详细描述并展示了我作为初学者所理解的基本MVVM技术的应用。在这里,我们通过全局描述一个具有更多交互的桌面应用程序来展示MVVM的优势。使用标准技术描述一个稍大的应用程序不是那么基础,并且有点冗长。它主要作为MVVM的全局示例或对应用程序本身感兴趣的人的兴趣点。
最终程序有许多限制(仅限W7,非常临时的文件管理,没有拖放,上下文菜单等)。尽管有这些限制,但有些主题可能仍然有趣。
- 简要讨论我们想尝试的文件浏览器任务。
- MVVM桌面应用程序的一个全局示例,其中包含一些交互。
- 仅使用基本的MVVM技术,并额外使用一些附加属性和弹出窗口。
- 选项卡式文件夹面板。
- 窗口中央的通用“向上”文件夹和“关闭选定选项卡”按钮。
- 所有选项卡都可以使用命名按钮作为组保存/加载。
- 用于临时(无后台进程,无反馈)文件管理的按钮。
- 复制一个带日期的文件夹。
- 无外部DLL。
背景
首先,我们将简单介绍一下文件浏览器以及我们希望在原型中尝试的任务。在下一节中,我们将介绍一些附加属性。要了解更多操作,请直接跳到模型部分。
一个重要的问题是什么是最终的最佳文件浏览器。一个通用的文件浏览器必须满足所有用户。最好的特定文件浏览器取决于用户、他/她的经验和偏好以及当前的任务。考虑到文件浏览器对某些任务的重要性,可以考虑为不同的受众/任务提供多个文件浏览器。事实上,有相当多的通用文件浏览器,它们在功能和偏好上有所不同,例如请参阅维基百科和此处。
Windows 7文件浏览器非常成熟和完善,对我来说,它提升了Windows 7的用户界面体验。有关其W8继任者的一些初步设计考虑,请参阅此处。一如既往,文件浏览器的功能很重要,但用户也必须找到并学习如何使用这些功能的模式。Windows 7文件浏览器非常出色,我们不想取代它。我注意到在某些场景中,对我来说可能有一些小的改进,我想对此进行调查和实验。
- 我注意到在使用文件浏览器时存在一种模式。有些任务(2-4)需要一些固定文件夹与一两个可变文件夹结合。我希望一次性打开一个任务的所有2-5个选项卡组,并在它们之间进行复制/移动。
- 如果在导航树中展开组,则必须折叠树或在树中的组之间滚动。因此,我想体验选项卡式导航树的效果如何。
- 我有时会复制一个文件夹并添加日期。
- 使用两个文件夹面板进行复制和移动,就像DOS的Norton Commander。如果可以使用选项卡,则相关性会降低。
关于编写文件浏览器的文章已经有很多(快速的Google搜索会显示),但我只给出三个链接。第一个侧重于MVVM文件浏览器的设计,最后两个是Code Project上最近更新的功能性文件浏览器文章(对此我表示敬意)。
- WPF 101:一个简单的类似Windows资源管理器的目录浏览器,作者:Manoj Garg。
- WPF x 文件浏览器 x MVVM 作者:梁逸春。
- 向资源管理器树控件添加拖放功能 作者:Jim Parsells。
我们将只使用基本的MVVM技术。MVVM (Model-View-ViewModel) 是一种用于编写WPF程序的设计模式,当需要关注点分离和单元测试时。一篇经典文章是Josh Smith的"使用Model-View-ViewModel设计模式的WPF应用程序"。此处有一个简单的概述:Silverlight ViewModel样式:一个(过于)简化的解释。
我参考我的第一篇文章,其中我展示了一些`INotifyPropertyChanged`属性、`ObservableCollections`和`ICommands`及其在XAML中的绑定的代码片段。本文将不那么详细,我只列出ViewModel中的属性和命令。
关于附加属性的简短说明
在本文中,我还会使用一些自定义的附加属性。附加属性可以用于扩展那些没有所需或预期MVVM支持的WPF类。
许多WPF类都派生自DependencyObject类,该类提供依赖属性和附加属性的宿主支持。它们可以在XAML中指定,例如在<Button Height="20" Grid.Row="4"/>
中,Height
是一个依赖属性,而Row
是在Grid
类中定义的附加属性,由Button
类的一个实例托管。
可以创建自己的自定义附加属性(或者直接通过Google搜索找到一个可以解决常见问题的属性)。在定义附加属性时,我们可以在`RegisterAttached`方法中指定属性更改回调函数。
从回调函数的参数中,我们可以访问所附加的宿主类的实例(目标对象)以及属性的旧值和新值。我们可以调用方法、设置属性、测试目标的类、检索其数据上下文,在附加时我们可以挂钩目标的事件,在分离时可以解除这些事件。通过这种方式,我们拥有一个强大的方法来扩展WPF类的行为,并且附加属性可以添加到所有受支持的类中的XAML中。
附加属性可以用于将事件绑定到命令,以及在拖放框架中(绑定到所有相关的拖放事件并在某些位置执行用户指定的代码)。一些与附加属性的使用相关的链接以及我们将使用的附加属性。
- AttachedCommandBehavior V2 aka ACB 作者 Marlon Grech。
- 在MVVM模型中将事件转换为命令重访 作者 Zeeshan Amjad。
- 我如何在数据绑定的ItemsControls之间拖放项目? 作者 Bea Stollnitz。
- WPF控件开发揭秘第6章“附加属性的力量”包括“使用附加属性实现拖放”。另请参阅此处。
- 在MVVM中,如何实现数据网格的鼠标双击或单击? 用户 Lucas 的回答。
- 将任何WPF事件与ViewModel上的命令绑定,带参数 作者 Naveen Dhaka,另请参阅Sacha Barber的观点。
如果适用,Microsoft Expression Blend 是首选替代方案。例如,使用 Interaction.Triggers + EventTrigger 将命令绑定到事件。但是,需要外部 DLL(Microsoft Expression Blend SDK 中的 `Microsoft.Expression.Interactions`、`System.Windows.Interactivity`)。另请参阅 Sacha Barber 的 WPF:Blend 3 交互/行为 或 Pencho Popadiyn 的 Silverlight 中的行为和触发器。
关于设计与实现过程的简短说明
我以前的专业主要是科学算法的设计和优化,没有太多的正规IT背景。我从一个关于应用程序应该是什么样子的全局想法开始,参见上面的图。我只想添加一个选项卡式文件夹面板并保存/加载选项卡组,但最后一刻我决定使用按钮(拍摄快照、复制、移动、新建文件夹、重命名、删除等)添加一些非常临时的文件管理功能。
设计的起点是图,由此产生了我们用于模型(NavTreeItem、FolderPlane、FolderPlaneItem)和ModelView的类。一个更常见的方法是从系统类(ShellItems、Drives、Folders、Directories等)开始。我们可以说我们的设计更多地采用了草图/视图的视角。所选择的方法似乎导致了最初更简单的设计,但是需要额外的转换才能访问所有项目(Libraries、Favorites等,未研究Shell),并且在从/向其他应用程序拖放时可能需要额外的转换。我们可以在我们的类中合并(链接到)系统类。
尽管我们在文章中按模型、ViewModel和视图的顺序描述了应用程序,以强调MVVM中关注点分离的优点,但实际上存在大大小小的迭代,并且尽快通过测试或应用程序部分的可视化呈现来获得一些反馈是明智的。
我采用了增量和迭代的方法。通过在不同规模上添加新功能来增加复杂性。从一个导航树开始,添加选项卡式导航树,一个文件夹平面,选项卡式文件夹平面等。较小的迭代是添加一些新功能,使其工作,通过点击(非常轻量的TDD)进行一点测试,然后进行重构步骤。由于关注点分离和逐步增加功能,这工作得很好,但回顾起来,`SelectedPath`和`SelectedFolderPlane`的设置设计如果事先有一个更明确的设计,而不是修复某些场景,将会受益。
作为WPF初学者,选择您想要使用的XAML需要一些时间,并且处理事情不如预期的情况也需要一些时间。例如,SelectedItem(s) 是一个只读属性,您无法绑定;已经选择的项不会再次触发;数据网格行双击没有命令;选定项不在视图中;数据网格的选定项在拖动时似乎会弄乱等等。幸运的是,Google是您最好的朋友,社区中有很多解决方案,大多数时候您只需要选择第一个可用的解决方案,或者如果有时间,选择最好的解决方案。
我将仅使用我作为初学者理解的标准基本MVVM技术来制作这个原型。我不会使用外部(Blend)DLL或MVVM框架等常见实践。我使用了一个主视图模型(尚未重构),为了组织XAML,我将XAML代码移到了“用户控件”中,这些用户控件仍然需要视图模型中的类。请参见下图了解项目结构。
模型:添加FolderPlane、FolderPlaneItem、SavedFolderTabsItem
类`NavTreeItem`在我的上一篇文章中进行了讨论。在这里我们添加了3个新类:`FolderPlane`、`FolderPlaneItem`和`SavedFolderTabsItem`。请参阅下面的代码,了解`FolderPlane`类和`FolderPlaneItem`类的接口。
public interface IFolderPlane
{
string FullPathName { get; set; }
// For display (in TabItem)
string FriendlyName { get; set; }
// Sets the FolderPlaneItems
void SetFolderPlane(string path, bool clear = false);
void RefreshFolderPlane();
//constructor: FolderPlane (string path);
// Items displayed in FolderPlane
ObservableCollection<FolderplaneItem> FolderPlaneItems { get; set; }
}
public class FolderPlaneItem
{
public String FullName { get; set; }
public String Name { get; set; }
public String Ext { get; set; }
public String Date { get; set; }
public long Size { get; set; }
public BitmapSource MyIcon { get; set; }
}
我们只打算在文件夹面板的视图中提供“详细信息”列表选项。为了提供更多的文件夹面板视图选项,模型(在`FolderPlane`中添加`ViewOption`属性,有一个`FolderPlaneItem`类和派生类,如`FolderPlaneDetailsItem`)、ViewModel(添加`ViewFolderPlaneOption`属性)和视图(新DataTemplates)中需要一些扩展。
关于`SavedFolderTabsItem`类,请参见下面的代码。这个类将用于通过点击命名按钮来打开一组文件夹面板的选项卡。每个项目都代表一个组,其中包含一个用于显示其名称的`FriendlyName`和一个用于关联`FolderPlanes`的`TabFullPathName`集合。`ObservableCollection<SavedFolderTabsItem>`的保存和加载使用`XmlSerializer`类。public class SavedFolderTabsItem
{
public string FriendlyName { get; set; }
public Collection<string> TabFullPathName { get; set; }
}
ViewModel
属性
现在我们将讨论`MainVm`。该类分为两个文件(属性和命令),但应重构为更小的ViewModel。现在我们将讨论`MainVm`中的所有属性,请参阅下面的伪代码。
- `TabbedNavTrees`在我们的第一篇文章中已经讨论过,当点击树中的一个项目时,`SelectedPath`将被设置。
- 接下来,我们有属性`SelectedFolderPlane`和`FolderPlanes`。它们将在视图中绑定到`FolderPlaneView`和`TabsFolderPlanesView`。
- `SavedFolderTabs`和`SelectedIndexSavedFolderTabs`用于管理选项卡组。它们将在视图中绑定到`SavedFolderTabsView`。`SelectedIndexSavedFolderTabs`的setter将更新`FolderPlanes`。
- `ShaderOpacity` 属性将绑定到主网格的 Opacity。
- PopUp1IsOpen将在其setter中调整ShaderOpacity。它将被绑定到视图中的一个Popup,因此当PopUp1IsOpen设置为true时,Popup将打开,并且主网格的透明度将变暗。
- 最后,我们跟踪用于文件管理的`SelectedFolderItems`、`SelectedFolderItem`和`SnappedSelectedItems`。它们在命令中设置。
请注意,如果我们想要一个带有两个文件夹面板的文件浏览器,则可以通过一些工作来扩展属性,添加另一个`SelectedPath2`、`SelectedFolderPlane2`、`FolderPlanes2`和一个`bool`类型的`IsFolderPlane1Selected`。
// snippets from MainVm, pseudo code properties
public partial class MainVm : ViewModelBase
{
public TabbedNavTreesVm TabbedNavTrees { get; set;}
public string SelectedPath
public FolderPlane SelectedFolderPlane
public ObservableCollection<FolderPlane> FolderPlanes
public ObservableCollection<SavedFolderTabsItem> SavedFolderTabs
public int SelectedIndexSavedFolderTabs
// ShaderOpacy binding is used to set Opacity in MainWindow when a popup is open
public double ShaderOpacity
public bool Popup1IsOpen; Popup2IsOpen; Popup3IsOpen
// for file management
public List<String> SelectedFolderItems
public List<String> SnappedSelectedItems
public String SelectedFolderItem
}
我想在这里强调MVVM中关注点分离的优势。尽管`MainVm`的代码相当冗长且包含大量重复代码,但如果从属性数量来看,它却相当紧凑。这些属性并非分散在代码隐藏的各个位置,我们可以在属性的setter中添加功能。
示例Setter属性:SelectedPath
`SelectedPath` 在 `SelectedPathFromTreeCommand`、`FolderPlaneItemDoubleClickCommand`、`CloseTabCommand` 和 `FolderUpCommand` 等多个命令中设置。现在我们将讨论 `SelectedPath` 的 setter,它还包含设置 `SelectedFolderPlane` 的逻辑(设计选择)。
在解析快捷方式后,如果值为空字符串或指向现有项目,则 setter 会设置 `SelectedPath`。对于现有项目,它还会设置 `SelectedFolderPlane`。它会检查路径是否已存在于 `FolderPlanes` 中以防止重复,添加一个新选项卡或替换当前的 `SelectedFolderPlane`。请参阅下面的代码。
// from the setter of SelectedPath
public string SelectedPath
{ .....
string testValue = FolderPlaneUtils.ResolveIfShortCut(value);
.....
// implementation of the INotifyPropertyChanged, see first article:
if (!SetProperty(ref selectedPath, testValue, "SelectedPath")) return;
// Design Choice: If in FolderPlanes: set SelectedFolderPlane to that and done
int indexInPlanes = GetIndexFolderPlanes(selectedPath);
if (indexInPlanes != -1)
{
SelectedFolderPlane = FolderPlanes[indexInPlanes]; .... return;
}
FolderPlane newPlane = new FolderPlane();
newPlane.SetFolderPlane(selectedPath);
// UseCurrentPlane is set to true elsewhere ... when we move up or down a FolderPlane
// Design Choice now: replace current SelectedFolder in FolderPlanes by newPlane
if (UseCurrentPlane)
{
UseCurrentPlane = false;
indexInPlanes = GetIndexFolderPlanes(SelectedFolderPlane.FullName);
if (indexInPlanes != -1) {FolderPlanes[indexInPlanes] = newPlane;} else FolderPlanes.Add(newPlane);
}
else
{
FolderPlanes.Add(newPlane);
}
SelectedFolderPlane = newPlane;
...
ICommands列表
为了完整起见,我们只给出一个`ICommands`列表,请参见下面的伪代码。这些命令操作ViewModel中的属性,并将绑定到视图中。对于许多命令,指定了`canExecute`。绑定到这些命令的按钮仅在`canExecute`返回true时才启用。我们将对一些命令进行简短的全局非正式描述,以给人留下印象。
- `SelectedPathFromTreeCommand` 设置 `SelectedPath`,并在单击树项目时调用。
- `FolderPlaneItemDoubleClickCommand` 检查点击的项目是否为文件夹。如果是,则将 `SelectedPath` 设置为该文件夹。否则,它会启动该项目。当双击文件夹平面数据网格的某一行时调用。
- `CloseTabCommand` 和 `FolderUpCommand` 设置 `SelectedPath`。
- 当按下某些按钮时,`ToggleOpenPopupCommands` 会被调用。它们会切换 `PopupIsOpen` 属性。
- 当数据网格的`SelectedItems`发生变化时,`SelectedItemsChangedCommand`会被调用。
- 一些文件管理命令。`FileManagementButtonsView` 中的按钮将绑定到这些命令。
// pseudo code, snippets from MainVm, ICommands
public partial class MainVm
{
public ICommand SelectedPathFromTreeCommand
public ICommand FolderPlaneItemDoubleClickCommand
public ICommand CloseTabCommand, FolderUpCommand
public ICommand ToggleOpenPopup1Command, ToggleOpenPopup2Command, ToggleOpenPopup3Command
public ICommand AddSavedTabsCommand, DeleteSavedTabsCommand, CloseAllFolderTabsCommand
// Commands for FileMangement
public ICommand SelectedItemsChangedCommand
public ICommand SnapShotSelectedCommand
public ICommand CopySnapShotCommand, MoveSnapShotCommand, CopySnapShotAddDateCommand
public ICommand NewFolderCommand, RenameCommand, DeleteSelectedCommand
public ICommand RefreshFolderPlanesCommand
}
请注意,`MainVm` 还包含一些支持最小文件管理的过程。然而,这些过程非常临时。1)它们不提供用户反馈,2)它们未在后台进程中实现,因此在复制大型文件时,UI会挂起。我尚未研究像C# 5.0/NET 4.5的Async这样的东西是否能提供帮助。
视图
MainViewWindow
在视图中,我们使用XAML指定如何呈现数据,并绑定ViewModel中所有相关的属性(ObservableCollection、INotifyPropertyChanged和ICommand)。我使用“用户控件”只是为了组织XAML代码。首先,XAML代码作为MainViewWindow的一部分进行开发,测试后,我通过将其移动到“用户控件”来重构XAML代码,该用户控件仍然需要它们可以绑定到的特定ViewModel/DataContext属性。请参阅下面的代码,了解MainViewWindow的一些代码片段。MainVM被设置为Datacontext,其所有(子)属性将在“用户控件”的数据绑定中匹配。
- `TabbedNavTreesView` 用于选择一个 TreeItem。
- `FolderPlaneView`用于显示`FolderPlaneItems`,双击可更改选定文件夹或启动项目。
- TabsFolderPlanesView 用于从打开的选项卡中选择一个 SelectedFolderPlane,以及关闭和向上切换当前选项卡的按钮。
- `SavedFolderTabsView`用于通过命名按钮打开组中的所有文件夹选项卡,关闭所有文件夹选项卡,删除或保存所有当前选项卡的命名按钮。
- `FileManagementButtonsView`,用于“拍摄快照”、“复制快照”、“移动”、“删除选中项”等按钮。
// Snippets from MainViewWindow, only "UserControls" shown
// Grid definitions, Row and Columns properties, GridSplitter etc. ommited
<Window.DataContext>
<vm:MainVm x:Name="MyMainVm"/>
</Window.DataContext>
...
<Grid ... Opacity="{Binding ShaderOpacity}" >
...
<TextBlock Text="{Binding SelectedPath}" ... />
...
<vw:SavedFolderTabsView />
...
<vw:TabbedNavTreesView DataContext="{Binding TabbedNavTrees}"/>
...
<vw:TabsFolderPlanesView/>
...
<vw:FolderPlaneView/>
...
<vw:FileManagementButtonsView/>
...
文件夹面板视图
请参阅下面`FolderPlaneView`的一些代码片段,它由一个`Datagrid`组成。我们看到`ItemsSource`绑定到`SelectedFolderPlane.FolderPlaneItems`,`DataGridColumns`绑定到`FoderPlaneItem`类的属性(MyIcon,Name等)。
数据网格没有针对双击行设置标准命令。`DoubleClickCommand`是一个附加属性,如前所述。它挂钩到`DataGrid.MouseDoubleClick`事件。当双击数据网格行时,会调用`MainVm`中的`FolderPlaneItemDoubleClickCommand`,参数为`CurrentItem`。此命令会启动一个应用程序,或者如果是文件夹,则设置`SelectedPath`。
DataGrid的`SelectedItems`是一个只读属性,无法直接绑定到`MainVm`中的属性。CommandExecuter提供了一些附加属性,并在`SelectionChanged`事件发生时调用`SelectedItemsChangedCommand`,参数为`SelectedItems`。该命令更新`MainVm.SelectedItems`属性,该属性将用于临时文件管理命令中。
// snippets from "UserControl" FolderPlaneView
....
<DataGrid
ItemsSource="{Binding SelectedFolderPlane.FolderPlaneItems}"
SelectionMode="Extended" SelectionUnit="FullRow"
vw:DataGridDoubleClick.DoubleClickCommand="{Binding FolderPlaneItemDoubleClickCommand}"
....
mvvm:CommandExecuter.OnEvent="SelectionChanged"
mvvm:CommandExecuter.Command="{Binding SelectedItemsChangedCommand}"
mvvm:CommandExecuter.CommandParameter="{Binding Path=SelectedItems,ElementName=dataGrid1}" >
<DataGrid.Columns>
<DataGridTemplateColumn Width="SizeToHeader" Header=" Ico " IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Image Width="16" Height="16" Source="{Binding MyIcon, Mode=OneTime}"/>
.........
</DataGridTemplateColumn>
<DataGridTextColumn Width="*" Binding="{Binding Name, Mode=OneTime}" Header=" Name "/>
<DataGridTextColumn Width="SizeToHeader" Binding="{Binding Ext, Mode=OneTime}" Header=" Extension "/>
..........
选项卡文件夹面板视图
TabsFolderPlanesView显示FolderPlanes和SelectedFolderPlane。见下图。从左到右我们看到
- 两个按钮,关闭(红色)和文件夹向上(黄色)选定选项卡。
- 一些选项卡/文件夹面板,最后一个被选中。可以通过点击该选项卡来选择另一个文件夹选项卡。
- 一个下拉按钮,用于从列表中选择文件夹选项卡,就像在Visual Studio中一样。
文件夹选项卡可以通过拖放进行组织。(请注意,我们可以通过单击树项来打开新的文件夹选项卡,或者通过使用已保存的命名按钮在SavedFolderTabs中打开一组选项卡)。滚动条经过了一些调整,因此右侧可以选择只显示两个小按钮。
在这里,关闭和向上文件夹的选定选项卡按钮固定在窗口中央,形成一个“热区”,用于上下移动(通过单击FoldersPlaneItems)树,并节省文件夹选项卡的空间。通常,关闭按钮放置在每个选项卡上。还要注意,对于较长的文件夹名,我们尝试用换行符替换空格,以限制选项卡的宽度(参见最后选定的文件夹选项卡“Program Files”,分为2行)。
文件夹选项卡由一个ListBox组成,请参见我的第一篇文章。ListBox派生自ItemsControl类,可以为控件本身及其项目/子项设置属性。请参见下面的代码。
- 我们看到在ListBox中,有到`FolderPlanes`、`SelectedFolderPlane`和`FriendlyName`的数据绑定(使用DataTemplate)。
- `ButtonOnlyScrollViewer` 用于调整后的 ScrollViewer。
- `FolderPlaneTabs`负责呈现选定/未选定的文件夹选项卡。
- `FolderPlanesHeaders_SelectionChanged`是我唯一的代码隐藏(使用`lb.ScrollIntoView(..)`和`lb.UpdateLayout()`),用于确保当通过下拉列表选择文件夹选项卡时,选定的文件夹选项卡会进入视图。
- `ItemsPanel`已更改为水平`StackPanel`。
我想通过在这些列表内拖放来组织选项卡(ListBox)的顺序。作为练习,我编写了我的第一个附加属性,具有非常基本的拖放功能,没有视觉反馈。由于`vw:DnDSortOneListBox.Attach1="True"`附加属性,选项卡(ListItems)可以在列表中拖放。然而,这是一种针对仅涉及ListBox的特定情况的临时解决方案,我们应该为整个应用程序引入MVVM拖放框架。
// snippets from ListBox TabsFolderPlanesView "UserControl" <DataTemplate x:Key="FolderPlaneHeader" DataType="{x:Type mdl:FolderPlane}"> <TextBlock ... Text="{Binding FriendlyName}" ToolTip="{Binding FullPathName}"/> </DataTemplate> ... scrolviewer stuff ... style FolderPlaneTabs ... CloseCurrentTab, FolderUp buttons <ListBox ItemsSource="{Binding FolderPlanes}" SelectedItem="{Binding SelectedFolderPlane, Mode=TwoWay}" ItemTemplate="{StaticResource FolderPlaneHeader}" IsSynchronizedWithCurrentItem="True" Template="{StaticResource ButtonOnlyScrollViewer}" ItemContainerStyle="{StaticResource FolderPlaneTabs}" SelectionChanged="FolderPlanesHeaders_SelectionChanged" vw:DnDSortOneListBox.Attach1="True"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <StackPanel Orientation="Horizontal"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ListBox>
右侧按钮是一个下拉按钮,显示一个选项卡列表供选择。请参阅下面的代码。我使用菜单和组合框的一个小技巧,用不多的XAML代码实现了一个下拉列表。然而,Width、MaxWidth和Margin没有经过足够鲁棒的调整,以获得一个可接受的外观。
<Button Width="26" Height="20" Grid.Column="2"
Focusable="False" ToolTip="List tabs and select">
<!-- Hack, compact XAML, Menu with ComboBox; Width, MaxWidth and Margin tweaked -->
<Menu MaxWidth="20" Background="{StaticResource FolderTabs}">
<ComboBox
Margin="-6,-2,-6,-2"
DisplayMemberPath="FullPathName"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding FolderPlanes}"
SelectedItem="{Binding Path=SelectedFolderPlane, Mode=TwoWay}"/>
</Menu>
</Button>
...
已保存的文件夹选项卡视图
SavedFolderTabsView提供了通过一次点击打开已保存的命名文件夹面板选项卡组的可能性。请参见下图,从左到右我们看到:
- “Temp”和“Fe project”是命名为“按钮”的,用于打开一组文件夹选项卡。
- “关闭所有”按钮,关闭所有打开的文件夹选项卡。
- 删除按钮,删除一个命名的“按钮”,打开一个弹出窗口。
- 保存按钮,将所有当前文件夹选项卡保存为一个命名的“按钮”,打开一个弹出窗口。
这些命名“按钮”可以通过拖放进行排序,当选择另一个命名“按钮”时,新的顺序会保存。请注意,更新命名组的支持是临时的,您必须即兴发挥:打开一个组,重新组织选项卡式文件夹面板并保存新组。请注意,可以使用XAML将命名按钮替换为例如鼠标悬停时展开的小彩色圆圈。
命名的“按钮”又是一个ListBox,它使用水平StackPanel作为其ItemsPanel。其`ItemsSource`绑定到`MainVm.SavedFolderTabs`属性,`SelectedIndex`绑定到`MainVm.SelectedIndexSavedFolderTabs`。ItemTemplate / DataTemplate绑定到`SavedFolderTabsItem`的`FriendlyName`。
在这个简单的MVVM示例中,我们使用弹出窗口而不是模态窗口来保持简单。MVVM框架支持模态窗口,或者参见此处(尚未研究)。请参见上图和下面的保存按钮代码。当单击该按钮时,会调用`ToggleOpenPopup2Command`,`Popup2IsOpen`被设置,`MainVm`类的`Opacity`降低。这意味着通过XAML中的绑定,主窗口变暗并出现一个弹出窗口。按下弹出窗口中定义的按钮会将`Popup2IsOpen`设置为false。
// Some snippets showing the use of a Popup for the Save Button <Button Command="{Binding ToggleOpenPopup2Command}" .. <Popup IsOpen="{Binding Popup2IsOpen}" PopupAnimation="Slide" StaysOpen="False"> ... <StackPanel Background="White"> <GroupBox Header="Add a new name to save all current FolderTabs:" ... > <StackPanel Orientation="Horizontal"> <TextBox Name="SaveItem" Text="Enter here.." ......../> <Button Content="Add" Command="{Binding AddSavedTabsCommand}" CommandParameter="{Binding Path=Text, ElementName=SaveItem}"/> </StackPanel> </GroupBox> <Button Content="Cancel" Command="{Binding ToggleOpenPopup2Command}" Margin="5"/> </StackPanel> </Popup> ...
文件管理按钮视图
最后一刻,我想实现一些临时的文件管理。我尝试将数据网格拖放到另一个文件夹选项卡,但是当开始拖动时,选定的项目会被取消选择,我现在理解为LeftMousedown改变了选择,这似乎是一个常见问题。我没有时间正确引入拖放功能,但是为了测试,我跟踪了选定的项目,所以我决定使用一些按钮进行临时的文件管理。请参见下面的文件管理按钮图。从左到右,我们看到这些按钮:
- 拍摄选定文件夹面板项目的快照
- 复制快照到当前选定文件夹
- 移动
- 复制快照(单文件夹),日期添加到文件夹名称中
- 新建文件夹
- 重命名(使用弹出窗口)
- 删除选定文件
- 刷新文件夹面板和导航树
- (显示属性,未实现)
圆形按钮的样式可以在MainResources.XAML中找到。我们设置了Background、Content(1-2个字符的字符串)和FontFamily。对于FontFamily,我选择了Segoe Script、Wingdings、WebDings、Marlett等,并使用OpenOffice选择并复制/粘贴Content字符串。这种按钮样式在视觉上并不吸引人,但对于第一个原型来说,它节省了一些时间。当按钮未启用时,其不透明度会降低。
在XAML中,我们将这些按钮绑定到MainVm中指定的命令。绑定到带有CanExecute测试的命令的按钮是条件启用的。例如,复制的正确顺序是选择项目,点击“拍摄快照”(Ctrl+V),点击其他文件夹选项卡,点击“复制”按钮。因此,在选择FolderPlaneItems之后,“拍摄快照”按钮启用,不透明度提高(参见上图),点击此按钮后,“复制”、“移动”、“带日期复制”(仅限单个文件)按钮启用,等等。
如何制作一个更成熟的文件浏览器
要将这个玩具变成一个功能更完善的文件浏览器,还有很多工作要做。
- 更多测试
- 后台文件管理,用户反馈。
- 在选定文件与其他选项卡之间以及从/向快照按钮进行拖放。视觉反馈。
- 可选地为整个应用程序引入MVVM拖放框架。
- 文件夹面板的上下文菜单。右键单击可高效复制/粘贴,显示属性。
- 实施历史记录。W7中的历史记录,如地址栏、TabsFolderPlane、TabbedNavTree。
- 选项卡式导航树中更多的根项目。
- 使用服务定位器实现模态窗口。
- 从/向其他应用程序复制/移动。
- 改进外观。按钮,文件夹选项卡弹出,选项卡式导航树作为无边框列表,在区域内弹出选项卡。
- 尝试多种设置。两个窗口,将选项卡式导航树制作成弹出窗口。
- 用户设置选项。
我们可以针对更通用的文件浏览器或更专业的文件浏览器。通用的W7文件浏览器非常出色。对我来说,对于所研究的特定任务,当前原型已经足够,这种方法是否具有更普遍的兴趣值得怀疑。一个新的项目似乎更相关,尽管在此应用程序中引入通用的MVVM拖放框架很有趣。
关注点
- 请注意,我有点偏颇,我喜欢Visual Studio和Notepad++中的选项卡,以保持一些笔记的打开。我引入了选项卡式树和选项卡式文件夹面板,以及只需单击一下即可加载一组文件夹选项卡的功能。
- 第一个观察是,使用上下文菜单、拖放以及历史记录的常见自动化操作都失败了。
- 操作方式是首先打开一些文件夹选项卡,可以通过点击树项目,也可以通过点击已保存的命名“按钮”一次性打开一个组。
- 该原型在复制带日期的文件夹这一特定任务上效率很高。打开一组已保存的文件夹,选择文件夹项目,点击“拍摄快照”按钮,(点击选项卡选择其他目标文件夹),点击“带日期复制”按钮。
- 我的意图是,通过这个应用程序,全局展示MVVM的优势之一——控制分离。请注意,模型和模型视图相当紧凑。
- 通过这个原型,我对WPF有了更多的了解。
- 通过这个原型,我学习了一些基本的MVVM和附加属性的使用。我现在知道选择MVVM框架的一些要求(向模型属性引入INotify,通过约定将方法映射到ICommands,支持ViewModels/用户控件的循环以及ViewModels之间的通信,模态窗口)。
- 实施时间比预期要长。向那些制作功能齐全的文件浏览器的人致敬!
- 引入更多功能并使这个原型更成熟是非常诱人的。
- 有一些有趣的任务(Async,引入MVVM拖放框架,更多ViewModels),但一个新项目似乎更相关。与极简主义界面的趋势相反,我喜欢编写一个具有更吸引人外观的新桌面应用程序。
- 我不确定W7桌面版的前景。W8将多快取代W7,升级费用是多少?Windows 8有两种模式,一种名为“桌面”,另一种名为“Metro style”,请参阅此处。桌面版会有哪些Express版本可用?免费的Express版本似乎针对“Metro”,但在提交本文之前,我看到了关于免费Visual Express 2012桌面版的好消息,请参阅此处。我对这个消息非常高兴。
限制
- 我仅在1台Windows 7 PC上进行了有限测试。
- 原型,功能最小化。
- 非常临时的文件管理,没有后台进程,没有拖放等功能。
- 已知一个无法重现的Bug。文件管理选定文件夹被错误地设置为应用程序目录(空字符串?)。
历史
2012-06-13 提交文章第一版。