65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF x FileExplorer3 x MVVM

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (28投票s)

2014年5月9日

MIT

12分钟阅读

viewsIcon

128636

downloadIcon

4708

FileExplorer是一款基于WPF的控件,具有Windows Explorer的大部分功能,您可以将其用于显示shell对象或任何层次结构数据。

引言

FileExplorer项目是一个基于WPF的控件,实现了Windows Explorer的大部分方面。它可以用于显示shell对象或任何层次结构数据。

此处提供的代码实现了

  • 一个独立的窗口,
  • 一个文件打开和文件保存对话框,以及
  • 一个文件夹选择器对话框。

FileExplorer控件的设计使其能够嵌入到您可能开发的应用程序的UI中。

目录

背景

我于2008年创建了WPF FileExplorer的第一个版本。该版本仅包含DirectoryTreeFileList,提供了许多所需的功能(但框架未提供)。下一个版本FileExplorer2于2010年发布。它支持不同的实体类型、更多控件,并通过异步加载不使用async关键字)和快速目录树查找提高了性能。

FileExplorer2版本在实现自定义文件浏览器控件方面取得了巨大进步。但该实现的缺点是它被设计为一个演示,没有自定义空间。FileExplorer2的设计有一个层次塔,使其无法以后进行改进。

2014年春季,我从头开始重新创建了第3版FileExplorer3。此版本提供了更多控件,支持同一浏览器中的不同条目类型,支持触摸,并且可以自定义。本文重点介绍FileExplorer3项目并描述其用法。

特点

  • DirectoryTree、FileList、Navigation、Breadcrumb、Sidebar、Statusbar控件。
  • NormalWindow、TabControl和ToolWindow显示模式。
  • 支持任何层次结构作为条目,不一定是基于IO的,也不仅限于System.IO或DirectoryInfoEx。
    (当前支持:System.IO、DirectoryInfoEx、GoogleDrive、OneDrive和DropBox)
  • 可以在一个窗口中合并不同类型的条目。
  • 某些行为可以通过ScriptRunner进行配置(例如,双击打开或选择)。
  • 拖放和多选,这次被重构为UIEventHub,一个可重用的、符合MVVM且支持触摸的控件。
  • FileList支持
    • 同一控件中的多个视图,请注意,它使用ListView.View属性,而不是为每个视图使用ItemsControl。
    • 可自定义列(可在ViewModel中定义)
    • 所有视图都支持使用GridViewHeader进行列排序和过滤。
    • 使用VirtualStackPanel和VirtualWrapPanel进行虚拟加载,以在需要时创建UIElement。
    • 使用EntriesHelper进行异步条目加载
    • 当文件系统引发更改时刷新。
  • 以MVVM友好的类似树的结构进行面包屑导航。

注意:在shell中,触摸拖放支持不起作用,除非它已初始化。您可以通过触摸并按住(2秒)手势来初始化此功能,然后再开始拖动。

下载

FileExplorer3MIT License下发布,并在此处提供。

注意:MIT许可证比LGPL和MS-PL更宽松。用户可以修改库而不必开源。

使用代码

编译源代码

FileExplorer3项目支持在线目录,如OneDrive或DropBox。这些服务需要身份验证密钥,源代码中未包含这些密钥。因此,您必须将Copy Of AuthorizationKeys.cs(在TestApp.WPF中)复制到AuthorizationKeys.cs才能使用OneDrive或DropBox。如果您获取gapi_client_secret.json并将其放在TestApp.WPF项目目录中,则可以启用GoogleDrive实现。请查阅Copy of AuthorizationKeys.cs文件以获取有关获取这些密钥的更多信息。

if (System.IO.File.Exists("gapi_client_secret.json"))
   
   //For demo only, one should embed it in the binary in real world application.
   using (var gapi_secret_stream = System.IO.File.OpenRead("gapi_client_secret.json")) 
   {
       _profileGoogleDrive = new GoogleDriveProfile(_events, _windowManager, gapi_secret_stream);
   }

请参阅关于禁用映射/取消映射按钮的章节,以便在不使用特定在线目录服务的情况下使用此项目。

Bootstrapper

FileExplorer项目内部使用Caliburn Micro框架。您是否在项目中使用此框架取决于您的项目,但无论如何都必须执行Caliburn Micro BootStrapper。在大多数情况下,使用包含的类就足够了。

<Application x:Class="Nuget.FileExplorer.WPF.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                     xmlns:uc="http://www.quickzip.org/UserControls"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary>
                    <uc:FileExplorerBootStrapper x:Key="boot" />
                </ResourceDictionary>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

Caliburn Micro MVVM框架

 

FileExplorer3实现基于Caliburn Micro框架。您可以使用ExplorerViewModelTabbedExplorerViewModel(用于多标签浏览器),框架会自动找到视图。例如,以下代码在TestApp.WPF的AppViewModel.cs的第110行。

_windowManager.ShowWindow(new ExplorerViewModel(_events, _windowManager, RootModels.ToArray()));

此代码使用CaliburnMicro中的WindowManager来显示浏览器窗口。Caliburn Micro框架会自动找到相应的视图(在此示例中为ExplorerView)。视图和ViewModel之间的关联通过MEFAppBootstrapper.cs中导入。

container = new CompositionContainer(
  new AggregateCatalog(
      AssemblySource.Instance.Select(x => new AssemblyCatalog(x))
             .OfType<ComposablePartCatalog>()
             .Concat(new ComposablePartCatalog[] { new DirectoryCatalog(".") }))
 );

CaliburnMicro的自动关联要求所有视图都存储在FileExplorer3的View目录中,并使用基于{ViewModelName}View.xaml的命名约定。工具窗口(Explorer.ToolWindow.xaml)不遵循此规则,因为您可以通过在ShowWindow()中传递“ToolWindow”作为上下文来创建它。

_windowManager.ShowWindow(new ExplorerViewModel(
  _events, _windowManager, RootModels.ToArray()), "ToolWindow");

在大多数情况下,您会希望ExplorerView成为您UI的一部分。这可以通过使用cal:View.Context附加属性来指定上下文来完成。

<ContentControl Name="Explorer" DockPanel.Dock="Left" cal:View.Context="ToolWindow" />

该项目还包括FilePickerDirectoryPickerViewModel

var filePicker = new FilePickerViewModel(_events, _windowManager, FileFilter, 
         FilePickerMode.Open, RootModels.ToArray());
if (_windowManager.ShowDialog(filePicker).Value)
{
    MessageBox.Show(String.Join(",", filePicker.SelectedFiles.Select(em => em.FullPath)));
}
//or...
new ScriptRunner().Run(new ParameterDic(),
  ScriptCommands.OpenFile(_windowManager, _events, RootModels.ToArray(), FileFilter, "demo.txt",
  (fpvm) => ScriptCommands.MessageBox(_windowManager, "Open", fpvm.FileName), ResultCommand.OK));

Explorer UserControl的用户可能需要在启动前初始化许多字段。这可以通过使用ExplorerInitializer来完成,该ExplorerInitializerTabbedExplorerViewModel的所有标签中都是可重用的。

public static IExplorerInitializer getInitializer(IWindowManager windowManager,
    IEventAggregator events, IEntryModel[] rootModels, params IViewModelInitializer<IExplorerViewModel>[] initalizers)
{
    var retVal = new ExplorerInitializer(windowManager, events, rootModels);
    retVal.Initializers.AddRange(initalizers);
    return retVal;
}
...
var initializer = getInitializer(_windowManager, _events, RootModels.ToArray(),
        new BasicParamInitalizers(_expandRootDirectories, _enableMultiSelect, _enableDrag, _enableDrop),
        new ColumnInitializers(),
        new ScriptCommandsInitializers(_windowManager, _events, profiles),
        new ToolbarCommandsInitializers(_windowManager));

var sr = new ScriptRunner();
sr.Run(Explorer.NewWindow(initializer, context, null), new ParameterDic());

其他MVVM框架

您也可以在不使用Caliburn Micro框架的情况下使用Explorer UserControl。只需使用Explorer UserControl并通过其ViewModel切换属性即可。例如,ToolWindow.xaml中就有一个用于此目的的示例代码。

<Window ...
        xmlns:uc="http://www.quickzip.org/UserControls"
        xmlns:conv="http://www.quickzip.org/Converters"
        xmlns:cal="http://www.caliburnproject.org" ...>
    <Grid>
        <Grid.Resources>
            <conv:EntryModelToStringConverter x:Key="emts" />
        </Grid.Resources>

        ....
        <!-- This is required to show dragging items -->
        <AdornerDecorator x:Name="PART_DragDropAdorner" Grid.ColumnSpan="3" />

        <uc:Explorer x:Name="explorer" Mode="ToolWindow" /> <!-- Explorer in tool window mode -->
        ...
        <StackPanel ...>
            <bc:UIEventAdapter.Processors>
                <bc:DragDropLiteEventProcessor />
            </bc:UIEventAdapter.Processors>
            
            ...<!--ViewModel.FileList.Selection.SelectedItems is IEntryModel-->
            <TextBlock Text="{Binding ViewModel.FileList.CurrentDirectory, 
                Converter={StaticResource emts},
                ElementName=explorer}" />
                    
            ...<!--ViewModel.FileList.Selection.SelectedItems is List<IEntryViewModel>-->
            <TextBlock Text="{Binding ViewModel.FileList.Selection.SelectedItems, 
                Converter={StaticResource emts},
                ElementName=explorer}" />
            
            ...<TextBlock x:Name="testDroppable" 
                   AllowDrop="True"
                   Text="{Binding Label}" >
              <bc:UIEventAdapter.Processors>
                <bc:DragDropEventProcessor EnableDrag="false" EnableDrop="true" />
                <bc:DragDropLiteEventProcessor EnableDrag="false" EnableDrop="true"
                                               EnableMouse="False" EnableTouch="True" />
              </bc:UIEventAdapter.Processors>

            </TextBlock>
        </StackPanel>
    </Grid>
</Window> 

无论哪种方式,您都必须在IEntryModel中指定根目录。这可以从配置文件中解析。
(IProfile为FileExplorer提供了访问IO、WebStorage或其他用户定义存储的内容的接口)

var _profileEx = new FileSystemInfoExProfile(events, windowManager);
rootDir = _profileEx.ParseAsync(System.IO.DirectoryInfoEx.DesktopDirectory.FullName).Result;

每个文件系统/目录服务都有一个配置文件

  • FileSystemInfoProfile - 使用System.IO命名空间访问
  • FileSystemInfoExProfile - 使用DirectoryInfoEx库访问
  • SkyDriveProfile - 使用Microsoft.Live库访问Microsoft OneDrive存储
  • DropBoxProfile - 使用DropNet库访问DropBox
  • 以及GoogleDriveProfile。使用gapi库访问Google Drive。

基于Web的配置文件需要用户名和密码。如果您想了解“如何操作”,请查看演示(以及“使用代码”部分)。通过使用MapUnmap按钮,可以包含或从UI界面中删除在线目录服务。

UIEventHub

WPF UIEventHub控件注册了一系列事件并将它们分发给注册的元素:UIEventProcessor,可用的UIEventProcessor包括MultiSelectEventProcessorDragDropEventProcessor,这是FileExplorer2SelectionHelperFileDragDropHelper静态类的更新。
UIEventHub的文档在此处提供。

禁用映射/取消映射按钮

如果您根本不想使用这些在线API,则可以完全禁用映射/取消映射功能。您可以在TabWindow()方法中找到TestApp.WPFScriptCommandsInitializersToolbarCommandsInitializers的调用。

 var profiles = new IProfile[] {
                _profileEx, 
                //Comment these if you don't need them.
                _profileSkyDrive, _profileDropBox, _profileGoogleDrive
            };

            var initializer = getInitializer(_windowManager, _events, RootModels.ToArray(),
                ...
                new ScriptCommandsInitializers(_windowManager, _events, profiles),
                new ToolbarCommandsInitializers(_windowManager));

            var tabVM = new TabbedExplorerViewModel(initializer);
            
            _windowManager.ShowWindow(tabVM);

在ScriptCommandsInitializers.InitalizeAsync()中

if (_profiles.Length > 0)
      explorerModel.DirectoryTree.Commands.ScriptCommands.Map =
          Explorer.PickDirectory(initilizer, _profiles,
          dir => Explorer.BroadcastRootChanged(RootChangedEvent.Created(dir)), 
          ResultCommand.NoError);

在ToolbarCommandsInitializers中,您可以找到以下内容

 explorerModel.DirectoryTree.Commands.ToolbarCommands.ExtraCommandProviders = new[] { 
       new StaticCommandProvider(
         ...
        new CommandModel(ExplorerCommands.Map)  { 
             Symbol = Convert.ToChar(0xE17B), 
             IsEnabled = true,
             IsHeaderVisible = false, IsVisibleOnToolbar = true
         },
         new CommandModel(ExplorerCommands.Unmap)  { 
             Symbol = Convert.ToChar(0xE17A),
             IsVisibleOnMenu = true,
             IsVisibleOnToolbar = true
         }

您可以删除这些调用以禁用映射功能。

IProfile

您可以实现IProfile接口来显示您自己的层次结构信息。

IProfile接口包括

  • CommandProvider(提供CommandModel),
  • PathHelper(System.IO.Path的替代),
  • HierachyComparer(比较两个IEntryModels的层次结构),
  • MetadataProvider(提供MetadataModel),以及
  • DragDrop(拖放支持)。

Parse和Listing(IProfile.Parse/ListAsync())方法可用于返回1个或多个IEntryModelIEntryModel仅包含基本属性,但您的实现可以定义更多属性(例如,Size),因为这些是由接口用户定义的。FileList中的列是完全可自定义的,无法硬编码。

自定义元数据

您可以根据当前选定的项目在sidebarstatusbar中显示不同类型的信息。

public enum DisplayType { Auto, Text, Number, Link, DateTime, TimeElapsed, Kb, Percent, Filename, Boolean, Image }
//These are defined in FileExplorer.WPF\Themes\Common\DisplayTemplatex.xaml
//Auto - (Default) Determine automatically depend on type of value
//Text - Given a string, display text.
//Number - Given a int/float, display ###,###,###,##0.##
//Link - Given a text, display Hyperlink 
//DateTime - Given a DateTime, display "yyyy/MM/dd HH:mm:ss" editable in Converters.xaml
//TimeElapsed - Given a TimeSpan, display xx years xx days zz hours
//Kb - Given a long, display 12.34kb (also support Mb and Gb)
//Percent - Given a short, display Percentage bar
//Filename - Given a FilePath string, display FileName only
//Boolean - Given a boolean, display a CheckBox
//Image - Given an url or ImageSource, display a image.

/*
  FileBasedMetadataProvider is defined in FileSystem.IO, you can include your own MetadataProvider using the explorer view model. 
  e.g.  explorerViewModel.Sidebar.Metadata.ExtraMetadataProviders = new [] {
                new DummyMetadataProvider()
            };
*/
public class FileBasedMetadataProvider : MetadataProviderBase
{
    public override async Task&l;IEnumerable<imetadata>> GetMetadataAsync(IEnumerable<IEntrymodel> selectedModels, 
           int modelCount, IEntryModel parentModel)
    {
        ...
        if (selectedModels.Count() > 0)
        {
            ...
            //Display "Creation Time : 3 years 6 months 2 days ago" in sidebar
            retList.Add(new Metadata(DisplayType.TimeElapsed, MetadataStrings.strCategoryInfo,
                    MetadataStrings.strCreationTime, creationTime.ToLocalTime()) { IsVisibleInSidebar = true });

            //Display "Size : 1234kb" in sidebar.
            retList.Add(new Metadata(DisplayType.Kb, MetadataStrings.strCategoryInfo,
                    MetadataStrings.strSize, size) { IsVisibleInSidebar = true });
            ...
        }

        return retList;
    }

}

自定义命令

Profile类可以定义一个CommandProvider,它提供一个CommandModel,用于在菜单(MenuItem)和工具栏的上下文中实现命令。
您可以指定额外的

/*
   ExCommandProvider is defined in FileExplorer.IO, you can define additional command provider using the following command:
   explorerModel.FileList.Commands.ToolbarCommands.ExtraCommandProviders = new[]
                new AdditionalCommandProvider(),
                ...
}
*/
public class ExCommandProvider : ICommandProvider
{
    FileSystemInfoExProfile _profile;
    public ExCommandProvider(FileSystemInfoExProfile profile)
    {
        _profile = profile;
    }

    public List GetCommandModels()
    {
        return new List()
                {                        
                    //Display OpenWith... Menuitem in Toolbar only and not in context menu.
                    new OpenWithCommandEx(_profile) { IsVisibleOnMenu = false, IsVisibleOnToolbar = true }
                };
    }
}

FileList

FileList视图以不同的视图模式显示项目和目录(可通过Parameters.ViewMode设置)。
FileList中的网格列是可自定义和可过滤的。它不仅在GridView中可见,在所有其他ViewModes中也可见。
FileList视图的显示不仅限于列。它还可以用于定义新视图以及点击时的操作。

自定义列

所有列都在ColumnList中定义。我们可以使用Binding(例如,Type)并定义一个DataTemplate。

explorerViewModel.FileList.Columns.ColumnList = new ColumnInfo[] 
    {
        //From a template named GridLabelTemplate, this template should be placed in Themes\Common\ItemTemplates.xaml, 
        //Or a place discoverable by the ListViewEx.
        ColumnInfo.FromTemplate("Name", "GridLabelTemplate", "EntryModel.Label", 
             //You also need to define a comparer for sorting.
             new ValueComparer<IEntryModel>(p => p.Label), 200),   
        //EntryModel.Label is only uses as identifier for column filter in this case.
        

        //Value from a bindings relative to the IEntryViewModel, e.g. EntryModel.Description in this case.
        ColumnInfo.FromBindings("Type", "EntryModel.Description", "", new ValueComparer<IEntryModel>(p => p.Description), 200),
        ...
    };

...并定义FileList可以访问的模板。

<DataTemplate x:Key="GridLabelTemplate" >
        <StackPanel Orientation="Horizontal">
            <Image Source="{Binding Icon, Mode=OneWay}" Width="30" Height="30"  />

            <uc:EditBox x:Name="eb" Margin="5,0" DisplayValue="{Binding EntryModel.Label}" 
                     ...
                        />

        </StackPanel>
    </DataTemplate>

自定义列过滤器

FileExplorer3提供了ColumnFilters,以便用户可以指定要在文件列表中显示的项。

explorerViewModel.FileList.Columns.ColumnFilters = new ColumnFilter[]
{
    //Label, Path relative to IEntryViewModel and a match func
    ColumnFilter.CreateNew("0 - 9", "EntryModel.Label", e => Regex.Match(e.Label, "^[0-9]").Success),
    ColumnFilter.CreateNew("A - H", "EntryModel.Label", e => Regex.Match(e.Label, "^[A-Ha-h]").Success),
    ...
    ColumnFilter.CreateNew("Today", "EntryModel.LastUpdateTimeUtc", e => 
    {
       DateTime dt = DateTime.UtcNow;
       return e.LastUpdateTimeUtc.Year == dt.Year && e.LastUpdateTimeUtc.Month == dt.Month && e.LastUpdateTimeUtc.Day == dt.Day;
    }),
 };

自定义视图

IFileListViewModel.Parameters.View(字符串)更改时,文件列表会在xaml中查找属性+“View”,例如,Icon -> IconView。
所有视图都在Themes\Common\Views.xaml中定义,例如。

/*
  WPF's ListView support a <a href="http://msdn.microsoft.com/en-us/library/system.windows.controls.listview.view%28v=vs.110%29.aspx">View property that allow one to setting a ViewBase object in it.  
  The ViewBase is DependencyObject so it's bindable, so though bindings the panel (e.g. VirtualWrapPanel) can assess properties
  in the View object, while it can bind to the view model to obtain properties.
  
  Note: MetroUI's ListView does not have View property.  
*/
<uc:VirtualWrapPanelView x:Key="IconView"  
                        ColumnHeaderContainerStyle="{StaticResource ListViewExColumnHeaderContainerStyle}"
                        SmallChanges="{Binding Path=ItemHeight, RelativeSource={RelativeSource Self}}"                             
                        CacheItemCount="5"  
                             
                        ItemTemplate="{DynamicResource IconItemTemplate}"
                        ItemContainerStyle="{StaticResource IconItemContainerStyle}"
                        ItemHeight="{Binding Parameters.ItemSize}"
                        ItemWidth="{Binding Parameters.ItemSize}" 
                        HorizontalContentAlignment="Left" >
</uc:VirtualWrapPanelView>

注意:自UIEventHub更新以来,现在支持多选功能。即使在不支持IChildInfo的面板上也能工作。UIEventHub在这种情况下使用HitTest,正如MultiSelectHelper的第一个版本中所述。

自定义上下文菜单和工具栏命令

应用程序命令可以在配置文件中定义,也可以在Commands.ToolbarComands.ExtraCommandProviders中定义,如下面的代码示例所示。

explorerModel.FileList.Commands.ToolbarCommands.ExtraCommandProviders = new[] { 
     //Invoke RoutedUICommand - 
     //The following one shows Delete in menu, which execute ApplicationCommands.Delete when clicked.
     new CommandModel(ApplicationCommands.Delete) { IsVisibleOnMenu = true, Symbol = Convert.ToChar(0xE188) },

     //Multi Level - 
     //The following one below shows New\Folder (caption is overrided) in Menu, 
     //which execute ExplorerCommands.NewFolder when clicked.
     new DirectoryCommandModel(new CommandModel(ExplorerCommands.NewFolder) { Header = Strings.strFolder })
                        { IsVisibleOnMenu = true, Header = Strings.strNew, IsEnabled = true},     

     //Separator
     new SeparatorCommandModel(),

     //User defined - 
     //You can derive from CommandModel or DirectoryCommandModel to create your own command.
     new ViewModeCommand(explorerModel.FileList),
};

注意:对于DirectoryTree.Commands.ToolbarCommands,也存在类似的机制。.

自定义UICommands (ScriptCommandBindings)

IFileListCommandManager接口定义了许多可以在UI中使用的UICommand(命令)。

//Call Command Manager (ch/this)'s ScriptCommands (DynamicDictionary).Open IScriptCommand when
//ApplicationCommmands.Open is called.
//Noted that ch.ScriptCommands.Open is editable by user.
ScriptCommandBinding.FromScriptCommand(ApplicationCommands.Open, this, (ch) => ch.ScriptCommands.Open, 
   ParameterDicConverter, ScriptBindingScope.Local),
ScriptCommandBinding.FromScriptCommand(ExplorerCommands.NewFolder, this, (ch) => ch.ScriptCommands.NewFolder, 
   ParameterDicConverter, ScriptBindingScope.Local),
ScriptCommandBinding.FromScriptCommand(ExplorerCommands.Refresh, this, (ch) => ch.ScriptCommands.Refresh, 
   ParameterDicConverter, ScriptBindingScope.Local),
ScriptCommandBinding.FromScriptCommand(ApplicationCommands.Delete, this, (ch) => ch.ScriptCommands.Delete, 
   ParameterDicConverter, ScriptBindingScope.Local),

注意:IFileListCommandManager实现了IExportCommandBindings,该接口在IFileListViewModel和IExplorerViewModel.OnViewAttached()方法中,将RoutedUICommands(ExplorerCommands和ApplicationCommands)导出到视图,其范围决定了导出的位置。

public interface IScriptCommandBinding : INotifyPropertyChanged
{
   //(Optional) Script command to run when Command is execute.
   IScriptCommand ScriptCommand { get; set; } 
 
   //Command to execute when UICommandKey is executed, not necessary calling the script command.
   ICommand Command { get; set; }

   //RoutedUICommandBinding key (e.g. ApplicationCommands.Open).
   RoutedUICommand UICommandKey { get; } 

   //Create <a href="http://www.google.com.hk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=4&cad=rja&uact=8&ved=0CFIQFjAD&url=http%3A%2F%2Fmsdn.microsoft.com%2Fen-us%2Flibrary%2Fsystem.windows.input.commandbinding.aspx&ei=m2aDU8etOcXmkgXbzYD4CA&usg=AFQjCNHZmgFeS-Q6HkxC4hADnRO4nN203Q">binding for registered in UserControl.CommandBindings.
   CommandBinding CommandBinding { get; } 

   //Determine which usercontrol (e.g. Explorer or FileList) to register the command bindings.
   ScriptBindingScope Scope { get; } 
}

但其中一些命令实际上并未实现(删除)。

//If Selection Count > 1
//  If Selection[0] is Directory
//     OpenSelectedDirectory
//  Else do nothing
//Else do nothing
ScriptCommands.Open = FileList.IfSelection(evm => evm.Count() == 1,
        FileList.IfSelection(evm => evm[0].EntryModel.IsDirectory,    
            FileList.OpenSelectedDirectory,  //Selected directory
            ResultCommand.NoError),   //Selected non-directory
        ResultCommand.NoError //Selected more than one item.                   
        );
ScriptCommands.Delete = NullScriptCommand.Instance;

有些命令无法实现,因为它们不是IO特定的。例如,双击文件什么也不做。所以您需要自己实现。

explorerModel.FileList.Commands.ScriptCommands.Open =
    FileList.IfSelection(evm => evm.Count() == 1,
        FileList.IfSelection(evm => evm[0].EntryModel.IsDirectory,
            FileList.OpenSelectedDirectory, //Selected directory                        
            FileList.AssignSelectionToParameter(
                //This originally used by Toolbar (CommandModel), so it uses Parameter (selected entry)
                new OpenWithScriptCommand(null))),  
        ResultCommand.NoError //Selected more than one item, ignore.
        );
explorerModel.FileList.Commands.ScriptCommands.Delete =
        FileList.IfSelection(evm => evm.Count() >= 1,
        ScriptCommands.IfOkCancel(_windowManager, pd => "Delete",
            pd => String.Format("Delete {0} items?", (pd["FileList"] as IFileListViewModel).Selection.SelectedItems.Count),
            ScriptCommands.ShowProgress(_windowManager, "Delete",
                        ScriptCommands.RunInSequence(
                            FileList.AssignSelectionToParameter(
                                DeleteFileBasedEntryCommand.FromParameter), //Same as above
                            new HideProgress())),
            ResultCommand.NoError),
        NullScriptCommand.Instance);

我编写了IScriptRunner接口,以使实现更具可重用性和可读性。该接口运行IScriptCommand接口,并且已经提供了一些预定义的IScriptCommands

ScriptRunner

ScriptRunner类同步或异步地运行脚本命令。ScriptCommand基本上是一段要执行的代码。它可以返回另一个要随后执行的脚本命令。会话信息通过ParameterDic传递。ParameterDic是一个Dictionary<string, object>

FileExplorer3可用于在2个地方定义ScriptCommand,如下一节所述。

ScriptCommand作为CommandBindings(在UI中)

ScriptCommand可以在ContextMenu或Toolbar中使用。这需要Parameter(例如,pd["Parameter"])被设置为选定的entryModel(s)。UIParameterDic存储在ParameterDic中以支持此情况。UIParameterDic提供了Sender和EventArgs等附加属性。脚本命令通过ScriptCommandBindings注册到UICommand

in FileListCommandManager

    ScriptCommands = new DynamicDictionary<IScriptCommand>();
    ...
    ScriptCommands.Paste = vm.ScriptCommands.RunInSequence(
         //pm["Destination"] = File list current directory.
         FileList.AssignCurrentDirectoryToDestination(
             //pm["Parameter"] = File list selection.
             FileList.AssignSelectionToParameter(
               //Get DataObject from Clipboard, then uses currentDirectory (from GetCurrentDirectoryFunc)
               //'s profile to get entry models from the dataObject, and call the TransferCommandFunc as shown below.
               ClipboardCommands.Paste(ExtensionMethods.GetFileListCurrentDirectoryFunc,                 
                  (dragDropEffects, src, dest) => new SimpleScriptCommand("Paste", (pm) =>
                  {
                    //dest responsible for the transfer.
                    dest.Profile.DragDrop.OnDropCompleted(src.ToList(), null, dest, dragDropEffects);
                    return ResultCommand.NoError;
                  })))
            )
    );

    ...
    exportBindingSource.Add(
        new ExportCommandBindings(            
        ScriptCommandBinding.FromScriptCommand(ApplicationCommands.Paste, this, (ch) => ch.ScriptCommands.Paste, 
            ParameterDicConverter, ScriptBindingScope.Local),
        ));

    IEntryModel _currentDirectoryModel = null;
    ToolbarCommands = new ToolbarCommandsHelper(events,
        ...
        {
            ExtraCommandProviders = new[] { 
                new StaticCommandProvider(new CommandModel(ApplicationCommands.Paste)) 
            }
        };
}
in FileListViewModel

protected override void OnViewAttached(object view, object context)
{
    base.OnViewAttached(view, context);
        var uiEle = view as System.Windows.UIElement;
    Commands.RegisterCommand(uiEle, ScriptBindingScope.Local); //Register to UserControl.CommandBindings        
}

作为预定义命令的运行器(在VM中)

定义ScriptCommand的第二种方式是通过ViewModel实现。代码可以调用**{VM}.Commands.ScriptCommands.Execute/ExecuteAsync()**,在这种情况下,传递VMParameterDic。因此,ScriptCommand实际上可以从ParameterDic(例如,pd["FileList"]作为IFileListViewModel)访问调用者的ViewModel。

大多数命令都在ViewModel中定义。屏幕截图包含许多已定义的脚本命令。Commands基于ICommandManager接口,并可以在VM.Commands.ExecuteAsync()上运行,在这种情况下。

例如,让我们看看当Explorer收到根更改事件消息时(当用户映射或取消映射根目录时)会发生什么。Explorer执行Explorer.ChangeRoot()然后Explorer.Goto()

public void Handle(RootChangedEvent message)
{
    Queue<IScriptCommand> cmds = new Queue<IScriptCommand>();

    cmds.Enqueue(Explorer.ChangeRoot(message.ChangeType, message.AppliedRootDirectories));
    if (message.Sender != this)
        cmds.Enqueue(Explorer.GoTo(CurrentDirectory.EntryModel));
    else
        switch (message.ChangeType)
        {
            case ChangeType.Created:
            case ChangeType.Changed:
                cmds.Enqueue(Explorer.GoTo(message.AppliedRootDirectories.First()));
                break;
            case ChangeType.Deleted:
                cmds.Enqueue(Explorer.GoTo(RootModels.FirstOrDefault()));
                break;
        }

    Commands.ExecuteAsync(cmds.ToArray());
}

以下ViewModel

  • TabbedExplorer,
  • Explorer,
  • DirectoryTree,
  • FileList,
  • Sidebar

实现了ISupportCommandManager接口,该接口提供可以调用Execute/ExecuteAsync()的命令属性,但您也可以在ExplorerViewModel类中调用FileListDirectoryTree的命令。

UIEventHub类也经常使用IScriptCommand接口。例如,它调用MultiSelectEventProcessor

其他ViewModel

Explorer和TabbedExplorer (ExplorerViewModel)

ExplorerViewModel是托管一切的ViewModel。每个ExplorerViewModel都有自己的FileListDirectoryTreeBreadcrumbNavigationStatusbarSidebarViewModel

文件名过滤器在ToolWindowFilePicker中实现。您可以指定FilterStr,它将设置SelectedFilter。

evm.FilterStr = "Texts (.txt)|*.txt|Pictures (.jpg, .png)|*.jpg,*.png|Songs (.mp3)|*.mp3|All Files (*.*)|*.*";

CurrentDirectoryIEntryViewModel属性类型。您可以通过Icon属性(ImageSource)或通过实际条目的EntryModel属性(来自profile)获取其图标。

ExplorerViewModel类的内部和外部事件通过不同的EventAggregator(来自CaliburnMicro)分别处理。支持的事件包括:

  • RootChange,
  • EntryChanged(文件系统更改),
  • DirectoryChanged(当前目录)。

内部事件可以通过BroadcastEvent聚合器广播到外部事件。

在标签式浏览器中,任何时候只能有一个活动的浏览器(SelectedItem)。它支持标签重新排序(通过将一个标签拖到另一个标签来重新排序),方法是在创建新标签时设置DragHelperDropHelper

DragHelper = new TabControlDragHelper<IExplorerViewModel>(this);
//..when creating new tab
expvm.DropHelper = new TabDropHelper<IExplorerViewModel>(expvm, this);

这些帮助程序是可重用的。如果有一个tabControl并想支持标签重新排序,可以使用类似的方法。

DirectoryTree和Breadcrumb

DirectoryTree类和Breadcrumb(Tree)类非常相似,以至于我曾想将它们合并成一个类。但问题是展开模式不同。例如,Breadcrumb显示根路径中的未展开项,而DirectoryTree显示已展开的项。

//Breadcrumb
await Selection.LookupAsync(value,
    RecrusiveSearch<IBreadcrumbItemViewModel, IEntryModel>.LoadSubentriesIfNotLoaded,
    SetSelected<IBreadcrumbItemViewModel, IEntryModel>.WhenSelected,
    SetChildSelected<IBreadcrumbItemViewModel, IEntryModel>.ToSelectedChild);

//DirectoryTree
await Selection.LookupAsync(value,
    RecrusiveSearch<IDirectoryNodeViewModel, IEntryModel>.LoadSubentriesIfNotLoaded,
    SetSelected<IDirectoryNodeViewModel, IEntryModel>.WhenSelected,
    SetExpanded<IDirectoryNodeViewModel, IEntryModel>.WhenChildSelected);

这两个ViewModel都是层次结构的。根ViewModel有一个EntriesHelper,其中包含下一级的项,反之亦然。请参阅BreadcrumbTree文章以了解EntriesHelperBreadcrumbTree的工作原理。

导航、状态栏和侧边栏

StatusbarSidebar都使用IEntriesHelper<IMetadataViewModel>来显示元数据。IsVisibleInSidebar属性和IsVisibleStatusbar属性定义了实际的显示。支持广泛的元数据。目前不支持修改元数据。

结论

前一版本(FileExplorer2)总是缺少一个功能,即删除某些功能或更改FileExplorer的工作方式。没有统一的解决方案。我必须深入研究源代码才能告诉每个人如何更改它。本次更新的一个目的是提供尽可能多的自定义。这种自定义应该可以轻松实现,而无需更改源代码,从而使其不仅仅是一个演示项目。此实现旨在供其他项目使用。

参考

以及我的一些项目,它们使用了

  • DirectoryInfoEx - 列出shell和磁盘目录。
  • UIEventHub [文档] - 为控件提供多选、拖放、触摸手势和输入绑定支持。
  • HtmlTextBlock - 解析一组有限的HTML标签并将其显示为TextBlock。
  • BreadcrumbTree - 树形结构中的面包屑(也包括列表结构中的一个)。
  • Aero Titlebar - Aero主题中的标题栏组件。

历史

  • 2014年5月10日 - 初始文章。
  • 2014年5月29日 - 更新了截图和文本。
  • 2014年6月17日 - 二进制文件更新至v3.0.11。
  • 2014年7月4日 - 二进制文件更新至v3.0.15。
© . All rights reserved.