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

Silverlight MVVM Lib 和 FileUploader(使用 HttpHandler)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (36投票s)

2010年1月9日

CPOL

19分钟阅读

viewsIcon

200055

downloadIcon

2432

一个演示应用程序,展示了如何使用 Silverlight/HttpHandler/MVVM 上传文件,并包含 Silverlight MVVM 库和助手。

目录

引言

嗯,圣诞节已经过去几天了,我们又要回到工作岗位了。熟悉我的人都知道,我通常写 WPF 及其相关技术。我喜欢 WPF,但自从我上次接触它的“小兄弟”Silverlight 以来已经有一段时间了。我想我上次玩 Silverlight 还是在 2007 年,那时我写了这篇文章(我想在当时来说还算不错):Silverlight 1.1 的乐趣和游戏

所以我想是时候看看为什么 Silverlight 最近受到微软如此多的媒体和社区关注了,Silverlight 的未来,以及所有这些。我说未来就是做酷的事情,而无论那是什么技术。但总之。

这篇文章最初是我正在制作的一个更大的 SL4 应用程序中的一小部分,目的是理解和玩转 RIA Services,但它有点变形了,现在已经有了自己的生命。嗯,实际上,它现在差不多有两个有意识的实体了。

有意识的实体 1:一个小的 Silverlight 类库,用于辅助MVVM开发。

有意识的实体 2:我本来应该一直做的,也就是玩转新的 SL4 功能。为了做到这一点,我创建了一个 SL4 上传器,可以将文件上传到 Web 服务器上的一个文件夹,如果文件是图片,它还可以选择性地显示一个 SL3/4 的 `ChildWindow` 来展示上传的文件。

必备组件

附带的演示代码针对 SL4,并作为 VS2010 BETA 2 解决方案进行开发,所以您需要以下零碎的组件:

所有这些我都在这篇文章中使用了……对此我没有任何歉意,新东西才是好东西。

演示应用程序的功能

正如我所说,演示应用程序只是附带演示代码的一部分,它基本上是这样做的:这是一个 SL4 应用程序,其中包含一个 SL4 控件,该控件可以将文件上传到 Web 服务器上的一个文件夹,如果文件是图片,它还可以选择性地显示一个 SL3/4 的 `ChildWindow`。

运行时看起来是这样的

用户选择一个文件

文件被上传(异步),并利用了 ASP.NET Handler。 `BusyIndicator`(Silverlight Toolkit)显示在 SL3/SL4 的 `ChildWindow` 中。

如果上传的文件是图片,它将显示在一个模态 `ChildWindow` 中(而 `ChildWindow` 默认情况下不支持这一点),如下所示

这涉及到许多巧妙的东西,比如服务、异步委托、调度程序上下文、构建请求/响应以及使用 ASP.NET Handlers。

正如我所说,演示应用程序只是其中一部分,第二部分(实际上是演示代码的副产品)是一个小的 Silverlight MVVM 框架,有助于以 MVVM 的方式创建 Silverlight 应用程序。我花了几天时间(可能 1-2 天的全部工作时间)就完成了这一切,所以它不是一个完整的 MVVM 框架,它不提供我的 Cinch WPF MVVM Framework 的所有功能,但它是一个非常好的开始。

在深入探讨演示应用程序(文件上传器)的工作原理之前,我将先谈谈构成附带演示代码一部分的小型 Silverlight MVVM 类库。

SL Mini MVVM 助手库

正如我所说,我为 WPF 编写了一个我认为相当不错的 MVVM 框架(Cinch WPF MVVM Framework),可能有些人会问为什么我不直接扩展 Cinch WPF MVVM Framework 并使其同时支持 SL 和 WPF。对我来说,两者之间有足够的差异,足以支持一个独立的框架。例如,当我编写 Cinch WPF MVVM Framework 时,SL 还没有 `IDataErrorInfo` 支持,也没有很多其他东西。它甚至无法打开弹出窗口。

所以在我看来,有足够的差异来拥有两个独立的框架。为此,我只做了最少的工作来创建一个我认为必要的、可行的 Silverlight MVVM 框架。

谁知道呢,也许有一天我会把两者合并,但现在,恐怕它们永远不会相交。

那么涵盖了哪些内容呢?嗯,将包括以下内容:

  • 命令
  • 消息传递
  • 服务
  • 线程辅助

我们将逐一介绍所有这些领域,但在那之前,我想感谢并表彰以下《WPF Disciples》的成员:

  1. Josh Smith / Marlon Grech:感谢他们出色的 `Mediator` 代码。
  2. Laurent Bugnion / Josh Smith:感谢 `RelayCommand/RelayCommand` 代码。
  3. Daniel Vaughan:感谢他关于 Silverlight `UISynchronizationContext` 的想法。

谢谢各位。

命令

我基本上使用的是 Laurent Bugnion 的修改版本(参见 Laurent 的 MVVMLight 项目),这是 Josh Smith 的 `RelayCommand` 和 `RelayCommand` 的版本。

以下是一个在 ViewModel 中使用它们的简单示例:

步骤 1:在 ViewModel 中声明 `ICommand` 属性。

public ICommand UploadCommand
{
    get;
    private set;
}

步骤 2:连接 `RelayCommand` / `RelayCommand`。

public ImageUploadViewModel()
{
    UploadCommand = new RelayCommand(ExecuteUploadCommand, CanExecuteUploadCommand);
}
...
...
private void ExecuteUploadCommand()
{
    //do stuff here
}

private Boolean CanExecuteUploadCommand()
{
    return true;
}

步骤 3:使用 ViewModel 暴露的 `ICommand` 属性。

<Button Content="Upload File" Command="{Binding UploadCommand}" />

消息传递

我重用了 Marlon Grech / Josh Smith 出色的 Mediator,这也是我的 Cinch WPF MVVM Framework 使用的。如果您没有阅读我的 **Cinch** 系列文章,以下是我当时关于 Mediator 的说法,这仍然直接适用于附带的 Silverlight MVVM 框架。

现在,我不知道您怎么想,但通常情况下,当我在处理 MVVM 框架时,我不会只有一个 ViewModel 来管理整个项目。实际上我有许多(事实上,我们有很多)。使用标准 MVVM 模式的一个问题是 ViewModel 之间的通信。毕竟,构成应用程序的 ViewModel 可能是各种各样不相关的对象,它们彼此之间一无所知。但是,它们需要了解用户执行的某些操作。这是一个具体的例子。

假设您有两个视图:一个显示客户,另一个显示客户的订单。比方说,订单视图使用了 `OrdersViewModel`,客户视图使用了 `CustomersViewModel`,当一个客户的订单被更新、删除或添加时,客户视图应该显示某种视觉提示,以提醒用户某个客户的订单详情已更改。

听起来很简单,对吧?然而,我们有两个独立的视图,由两个独立的 ViewModel 运行,没有任何联系,但显然,需要从 `OrdersViewModel` 到 `CustomersViewModel` 某种形式的连接,也许是某种消息传递。

这正是 Mediator 模式的意义所在。它是一个简单轻量级的消息传递系统。我很久以前就在我的 博客上写过这方面的内容,后来 Josh Smith / Marlon Grech(他们是一个原子对)把它做得更好,他们提出了您将在 **Cinch** 中看到的 Mediator 实现。

那么中介者是如何工作的呢?

这张图可能有所帮助:

这个想法很简单:Mediator 监听传入的消息,查看谁对某个特定消息感兴趣,然后调用已订阅到给定消息的每个对象。消息通常是字符串。

基本上,发生的情况是,有一个 `Mediator` 的单一实例(通常在 `ViewModelBase` 类中公开为静态属性),它等待对象订阅它,使用以下任一方式:

  • 整个对象引用。然后,任何标记了 `MediatorMessageSinkAttribute` 属性的 `Mediator` 消息方法都将在已注册对象上被找到(通过反射),并自动创建一个回调委托。
  • 一个实际的 Lambda 回调委托。

无论哪种情况,`Mediator` 都维护一个 `WeakAction` 回调委托列表。其中每个 `WeakAction` 都是一个委托,它使用内部的 `WeakReference` 类来检查 `WeakReference.Target` 是否为空,然后再调用委托。这考虑到了回调委托的目标可能不再存活的事实,因为它可能已经被垃圾回收了。指向不再存活对象的 `WeakAction` 回调委托的任何实例都会从 `Mediator` `WeakAction` 回调委托列表中移除。

当获得回调委托时,要么调用原始回调委托,要么调用标记了 `MediatorMessageSinkAttribute` 属性的 `Mediator` 消息方法。

以下是如何以所有可能的方式使用中介者的示例:

注册消息

使用显式回调委托(但这也不是我的首选方式)

我们只需创建正确类型的委托,并向 `Mediator` 注册一个消息通知的回调。

public delegate void DummyDelegate(Boolean dummy);
...

Mediator.Register("AddCustomerMessage", new DummyDelegate((x) =>
{
    AddCustomerCommand.Execute(null);
}));
注册整个对象,并使用 MediatorMessageSinkAttribute 属性

这是我最喜欢的方法,也是我认为最简单的方法。您只需将整个对象注册到 `Mediator`,并为您要接收消息的方法添加属性。

在 **Cinch** 中,注册是自动为您完成的,您无需做任何事情。只需继承自 `ViewModelBase` 即可。如果您想知道这是如何实现的,**Cinch** 中的 `ViewModelBase` 类会像这样注册到 `Mediator`:

//Register all decorated methods to the Mediator
Mediator.Register(this);

因此,任何标记了 `MediatorMessageSinkAttribute` 属性的方法都将在已注册对象上被找到(通过反射),并自动创建一个回调委托。这里有一个例子:

/// <summary>
/// Mediator callback from StartPageViewModel
/// </summary>
/// <param name="dummy">Dummy not needed</param>

[MediatorMessageSink("AddCustomerMessage", ParameterType = typeof(Boolean))]
private void AddCustomerMessageSink(Boolean dummy)
{
    AddCustomerCommand.Execute(null);
}

如何接收消息通知?

消息通知

这非常简单,我们只需使用 `Mediator.NotifyCollegues()` 方法,如下所示:

//Use the Mediator to send a Message to MainWindowViewModel to add a new 
//Workspace item
Mediator.NotifyColleagues<Boolean>("AddCustomerMessage", true);

任何订阅了此消息的对象现在都会被 `Mediator` `WeakAction` 回调委托列表调用。

服务

我猜想,因为我为我的 Cinch WPF MVVM Framework 编写了许多 UI 服务,所以我并不觉得为 Silverlight 编写一些新的服务很难。附带的演示代码使用了一些 Silverlight 特定的 UI 服务,我们稍后会讨论到。

但首先,我们需要知道如何添加正在使用的服务。为此,我借用了 Marlon Grech 的 ServiceLocator 实现,它很简单,就是在 `ViewModelBase` 类中有一个 `ServiceLocator` 实例,允许添加和移除新的 UI 服务。这是附带的 SL MVVM `ViewModelBase` 类的代码:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using GalaSoft.MvvmLight.Command;

namespace SLMiniMVVM
{

    public abstract class ViewModelBase : INotifyPropertyChanged, IDisposable
    {

        private static ServiceLocator serviceLocator = new ServiceLocator();

        /// <summary>
        /// Gets the service locator 
        /// </summary>
        public static ServiceLocator ServiceLocator
        {
            get
            {
                return serviceLocator;
            }
        }

        /// <summary>
        /// Gets a service from the service locator
        /// </summary>
        /// <typeparam name="T">The type of service to return</typeparam>
        /// <returns>Returns a service that was registered with the Type T</returns>
        public T GetService<T>()
        {
            return serviceLocator.GetService<T>();
        }
    }
}

以下是附带演示代码中服务的设置方式:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using SLMiniMVVM;

namespace TestSL4
{
    public partial class App : Application
    {

        public App()
        {
            this.Startup += this.Application_Startup;
            InitializeComponent();
        }

        private void Application_Startup(object sender, StartupEventArgs e)
        {
            //setup services
            ViewModelBase.ServiceLocator.
            RegisterService<ISLOpenFileService>(
                new SLOpenFileService(), true);

            ViewModelBase.ServiceLocator.
            RegisterService<ISLChildWindowService>(
                new SLChildWindowService(), true);

            ViewModelBase.ServiceLocator.
            RegisterService<ISLMessageBoxService>(
                new SLMessageBoxService(), true);

            //now register any popups
            ISLChildWindowService popupVisualizer = 
        ViewModelBase.ServiceLocator.GetService<ISLChildWindowService>();
            popupVisualizer.Register("ImagePopup", typeof(ImagePopup));
        }
    }
}

这是实际的代码。UI 服务在 `App.Startup` 上设置,但如果我们使用单元测试来测试 ViewModel,我们可以在单元测试的 `[Setup]` 部分(假设使用 NUnit)做类似这样的事情:

[Setup]
private void setup()
{
    //setup services
    ViewModelBase.ServiceLocator.
    RegisterService<ISLOpenFileService>(
        new TestOpenFileService(), true);

    ViewModelBase.ServiceLocator.
    RegisterService<ISLChildWindowService>(
        new TestChildWindowService(), true);

    ViewModelBase.ServiceLocator.
    RegisterService<ISLMessageBoxService>(
        new TestMessageBoxService(), true);

}

那么,附带的 Silverlight MVVM 库提供了哪些服务呢?嗯,有以下这些:

消息框服务

这与我在 Cinch WPF MVVM Framework 代码 / 如何使用 MessageBox 服务 中描述和展示的几乎一样,而这篇其他 Cinch 文章讨论了如何设置单元测试。

需要注意的是,由于 Silverlight 当前的一些限制,Cinch 提供的 WPF 服务和这个 Silverlight MVVM 库是不同的,但理念基本相同。阅读附带的代码注释/示例以及上面的链接,一切都会足够清晰。

以下是附带的 Silverlight MVVM `MessageBoxService` API 的样子:

/// <summary>
/// This interface defines a interface that will allow 
/// a ViewModel to show a messagebox
/// </summary> 
public interface ISLMessageBoxService
{
    /// <summary>
    /// Shows an error message
    /// </summary>
    /// <param name="message">The error message</param>
    void ShowError(string message);

    /// <summary>
    /// Shows an information message
    /// </summary>
    /// <param name="message">The information message</param>
    void ShowInformation(string message);

    /// <summary>
    /// Shows an warning message
    /// </summary>
    /// <param name="message">The warning message</param>
    void ShowWarning(string message);

    /// <summary>
    /// Displays a Yes/No dialog and returns the user input.
    /// </summary>
    /// <param name="message">The message to be displayed.</param>
    /// <returns>User selection.</returns>
    CustomDialogResults ShowYesNo(string message);

    /// <summary>
    /// Displays a Yes/No/Cancel dialog and returns the user input.
    /// </summary>
    /// <param name="message">The message to be displayed.</param>
    /// <returns>User selection.</returns>
    CustomDialogResults ShowYesNoCancel(string message);

    /// <summary>
    /// Displays a OK/Cancel dialog and returns the user input.
    /// </summary>
    /// <param name="message">The message to be displayed.</param>
    /// <returns>User selection.</returns>
    CustomDialogResults ShowOkCancel(string message);

}

打开文件服务

这与我在 Cinch WPF MVVM Framework 代码 / 如何使用 Open File 服务 中描述和展示的几乎一样,而这篇其他 Cinch 文章讨论了如何设置单元测试。

需要注意的是,由于 Silverlight 当前的一些限制,Cinch 提供的 WPF 服务和这个 Silverlight MVVM 库是不同的,但理念基本相同。阅读附带的代码注释/示例以及上面的链接,一切都会足够清晰。

以下是附带的 Silverlight MVVM `OpenFileService` API 的样子:

/// <summary>
/// This interface defines a interface that will allow 
/// a ViewModel to open a file
///
/// This is based on my WPF MVVM library called Cinch, which is available at
/// http://cinch.codeplex.com/
/// </summary>  
public interface ISLOpenFileService
{
    /// <summary>
    /// File
    /// </summary>
    FileInfo File { get; set; }

    /// <summary>
    /// Files
    /// </summary>
    IEnumerable<FileInfo> Files { get; set; }

    /// <summary>
    /// Filter
    /// </summary>
    String Filter { get; set; }

    /// <summary>
    /// FilterIndex
    /// </summary>
    Int32 FilterIndex { get; set; }

    /// <summary>
    /// FilterIndex
    /// </summary>
    Boolean Multiselect { get; set; }

    /// <summary>
    /// This method should show a window that allows a file to be selected
    /// </summary>
    /// <param name="owner">The owner window of the dialog</param>
    /// <returns>A bool from the ShowDialog call</returns>
    bool? ShowDialog();
}

子窗口服务

正如我在文章一开始提到的,我已经很久没用 Silverlight 了,现在出现了各种炫酷的功能。其中之一就是 `ChildWindow`,听起来很棒。不知道我在说什么?好吧,看看这篇帖子,它对 `ChildWindow` 做了一个很好的介绍:http://www.wintellect.com/CS/blogs/jprosise/archive/2009/04/29/silverlight-3-s-new-child-windows.aspx

现在,这一切都很好,但我们是优秀的软件开发者,我们不喜欢代码隐藏,因为它不可测试。所以我们需要考虑这一点,看看我们是否能实现一个 UI 服务,允许我们显示基于 `ChildWindow` 的 Silverlight 窗口,并且还能从它那里获得 `DialogResult`,还能在单元测试环境中模仿这种行为。

现在,如果您去阅读 `ChildWindow` API 文档并查找 `Show()` 方法,您会注意到 MSDN 说:“打开一个 `ChildWindow` 并返回,而不等待 `ChildWindow` 关闭。”

嗯,所以我们不仅要设计一个处理 `ChildWindow` 的酷炫服务,还要处理它不是模态的,因此不是阻塞的事实。

现在,我不知道您怎么想,但我相当喜欢我的弹出窗口(让我们面对现实吧,`ChildWindow` 就是这样,尽管它也会被用作一个花哨的进度指示器(就像我在附带的演示应用程序中所做的那样))是模态的,这样我知道当用户单击**确定**时,我会应用他们所做的更改,如果他们单击**取消**,我会丢弃他们的工作。

使用开箱即用的 `ChildWindow` 听起来可行吗?嗯,我认为不行。有什么办法可以解决吗?答案是,当然可以。

所以,我接下来要展示的是一个用于处理 `ChildWindow` 的 UI 服务,它可以作为无模式(非阻塞服务)工作,但只需稍加调整,也可以作为模态(阻塞)弹出窗口工作。

以下是附带的 Silverlight MVVM `OpenFileService` API 的样子:

/// <summary>
/// This interface defines a UI controller which can be used to display dialogs
/// in either modal or modaless form from a ViewModel.
///
/// This is based on my WPF MVVM library called Cinch, which is available at
/// http://cinch.codeplex.com/
/// </summary>   
public interface ISLChildWindowService
{
    /// <summary>
    /// Registers a type through a key.
    /// </summary>
    /// <param name="key">Key for the UI dialog</param>
    /// <param name="winType">Type which implements dialog</param>
    void Register(string key, Type winType);

    /// <summary>
    /// This unregisters a type and removes it from the mapping
    /// </summary>
    /// <param name="key">Key to remove</param>
    /// <returns>True/False success</returns>
    bool Unregister(string key);

    /// <summary>
    /// This method displays a modaless dialog associated with the given key.
    /// </summary>
    /// <param name="key">Key previously
    ///         registered with the UI controller.</param>
    /// <param name="state">Object state to associate with the dialog</param>
    /// <param name="completedProc">Callback used when UI closes (may be null), 
    /// which will have the Dialog state and result in it </param>
    void Show(string key, object state,
        EventHandler<UICompletedEventArgs> completedProc);

    /// <summary>
    /// This method displays a modal dialog associated with the given key.
    /// </summary>
    /// <param name="key">Key previously registered with the UI controller.</param>
    /// <param name="are">Wait handle to allow the popup to be shown Modally, and signal the
    /// blocked Waiting using code to continue</param>
    /// <param name="state">Object state to associate with the dialog</param>
    /// <param name="completedProc">Callback used when UI closes (may be null), 
    /// which will have the Dialog state and result in it </param>
    void ShowModally(string key, object state, AutoResetEvent are,
        EventHandler<UICompletedEventArgs> completedProc);
}

该服务的实际 Silverlight 实现看起来是这样的:

public class SLChildWindowService : ISLChildWindowService
{
    #region Data
    private readonly Dictionary<string, Type> _registeredWindows;
    #endregion

    #region Ctor
    /// <summary>
    /// Constructor
    /// </summary>
    public SLChildWindowService()
    {
        _registeredWindows = new Dictionary<string, Type>();
    }
    #endregion

    #region Public Methods
    /// <summary>
    /// Registers a collection of entries
    /// </summary>
    /// <param name="startupData"></param>
    public void Register(Dictionary<string, Type> startupData)
    {
        foreach (var entry in startupData)
            Register(entry.Key, entry.Value);
    }

    /// <summary>
    /// Registers a type through a key.
    /// </summary>
    /// <param name="key">Key for the UI dialog</param>
    /// <param name="winType">Type which implements dialog</param>
    public void Register(string key, Type winType)
    {
        if (string.IsNullOrEmpty(key))
            throw new ArgumentNullException("key");
        if (winType == null)
            throw new ArgumentNullException("winType");
        if (!typeof(ChildWindow).IsAssignableFrom(winType))
            throw new ArgumentException("winType must be of type Window");

        lock (_registeredWindows)
        {
            _registeredWindows.Add(key, winType);
        }
    }

    /// <summary>
    /// This unregisters a type and removes it from the mapping
    /// </summary>
    /// <param name="key">Key to remove</param>
    /// <returns>True/False success</returns>
    public bool Unregister(string key)
    {
        if (string.IsNullOrEmpty(key))
            throw new ArgumentNullException("key");

        lock (_registeredWindows)
        {
            return _registeredWindows.Remove(key);
        }
    }

    /// <summary>
    /// This method displays a modaless dialog associated with the given key.
    /// </summary>
    /// <param name="key">Key previously registered with the UI controller.</param>
    /// <param name="state">Object state to associate with the dialog</param>
    /// <param name="completedProc">Callback used when UI closes (may be null), 
    /// which will have the Dialog state and result in it </param>
    public void Show(string key, object state,
        EventHandler<UICompletedEventArgs> completedProc)
    {
        ChildWindow win = CreateWindow(key, state,false,null, completedProc);
        if (win != null)
        {
            win.Show();
        }
    }

    /// <summary>
    /// This method displays a modal dialog associated with the given key.
    /// </summary>
    /// <param name="key">Key previously registered with the UI controller.</param>
    /// <param name="are">Wait handle to allow the popup to be shown Modally, and signal the
    /// blocked Waiting using code to continue</param>
    /// <param name="state">Object state to associate with the dialog</param>
    /// <param name="completedProc">Callback used when UI closes (may be null), 
    /// which will have the Dialog state and result in it </param>
    public void ShowModally(string key, object state, AutoResetEvent are,
        EventHandler<UICompletedEventArgs> completedProc)
    {
        ChildWindow win = CreateWindow(key, state, true,are, completedProc);
        if (win != null)
        {
            win.Show();
        }
    }

    #endregion

    #region Private Methods

    private void DoCallback(ChildWindow sender, 
        EventHandler<UICompletedEventArgs> completedProc)
    {
        if (completedProc != null)
        {
            completedProc(sender, 
               new UICompletedEventArgs(sender.DataContext, sender.DialogResult));
        }
    }

    /// <summary>
    /// This creates the WPF window from a key.
    /// </summary>
    /// <param name="key">Key</param>
    /// <param name="dataContext">DataContext (state) object</param>
    /// <param name="isModal">True if the ChildWindow
    /// needs to be created modally</param>
    /// <param name="are">Wait handle to allow
    /// the popup to be shown Modally, and signal the
    /// blocked Waiting using code to continue</param>
    /// <param name="completedProc">Callback</param>
    /// <returns>Success code</returns>
    private ChildWindow CreateWindow(string key, object dataContext, 
        Boolean isModal, AutoResetEvent are,
        EventHandler<UICompletedEventArgs> completedProc)
    {

        if (string.IsNullOrEmpty(key))
            throw new ArgumentNullException("key");

        Type winType;
        lock (_registeredWindows)
        {
            if (!_registeredWindows.TryGetValue(key, out winType))
                return null;
        }

        var win = (ChildWindow)Activator.CreateInstance(winType);
        win.DataContext = dataContext;

        if (dataContext != null)
        {
            var bvm = dataContext as ViewModelBase;
            if (bvm != null)
            {
                bvm.CloseRequest += ((s, e) => win.Close());
            }
        }

        //if there is a callback, call it on Closed
        if (completedProc != null)
        {
            win.Closed +=
                (s, e) =>
                {
                    //if modal and there is a wait handle associated signal it to continue
                    if (isModal)
                    {
                        if (are != null)
                        {
                            DoCallback(win, completedProc);
                            are.Set();
                        }
                        else
                        {
                            DoCallback(win, completedProc);
                        }
                    }
                    else
                    {
                        DoCallback(win, completedProc);
                    }
                };

        }

        return win;
    }
    #endregion
}

所以,首先是简单的情况。哦,对于这些情况中的每一种,都假定以下内容:

  1. 您有一个 `ChildWindow` 的子类,名为 `ImagePopup`。
  2. Silverlight MVVM `OpenFileService` 已在 `App.Startup` 中按如下方式设置:
ViewModelBase.ServiceLocator.RegisterService<ISLChildWindowService>(
                             new SLChildWindowService(), true);
ISLChildWindowService popupVisualizer = 
   ViewModelBase.ServiceLocator.GetService<ISLChildWindowService>();
popupVisualizer.Register("ImagePopup", typeof(ImagePopup));

无模式(非阻塞)

显示 `ChildWindow` 子类就像这样简单,我们显示弹出窗口并设置一个回调委托,当 `ChildWindow` 子类的 `Closed` 事件发生时,就会调用该回调委托,如上面所示的 Silverlight 实现中所见。

synContext.InvokeSynchronously(delegate()
{
    popupVisualizer.Show("ImagePopup", this, (s, e) =>
    {
        if (!e.Result.Value)
        {
            msgbox.ShowInformation(
                "You clicked Cancel. So this could be used " + 
                "to delete the File from the WebServer.\r\n" +
                "Possibly on a new thread, Instead of showing " + 
                "this blocking Modal MessageBox");
        }
    });
});

并在单元测试中设置 `ISLChildWindowService` 服务(假设服务已按前面所述设置):

[Setup]
private void setup()
{
    //setup services
    ViewModelBase.ServiceLocator.
    RegisterService<ISLOpenFileService>(
        new TestOpenFileService(), true);

    ViewModelBase.ServiceLocator.
    RegisterService<ISLChildWindowService>(
        new TestChildWindowService(), true);

    ViewModelBase.ServiceLocator.
    RegisterService<ISLMessageBoxService>(
        new TestMessageBoxService(), true);

}

以下是我们如何设置它来模拟单元测试中的用户:

TestChildWindowService testChildWindowService =
  (TestChildWindowService)
    ViewModelBase.ServiceProvider.Resolve<ISLChildWindowService>();

//Queue up the response we expect for our given TestChildWindowService
//for a given ICommand/Method call within the test ViewModel
testChildWindowService.ShowDialogResultResponders.Enqueue
 (() =>
    {
        PopupDataContext context = new  PopupDataContext();
        context.SomeValue = 42;
        context.SomeOtherValue = "yes mate"
        return new UICompletedEventArgs(context, true);
    }
 );

为了完整起见,以下是 `TestChildWindowService` 代码的样子,说到底,它的工作方式与 `TestMessageBoxService` 大致相同。它只是使用回调 `Func` 来允许单元测试设置回调,以提供所需的用户模拟响应。

public class TestChildWindowService : ISLChildWindowService
{
    #region Data

    /// <summary>
    /// Queue of callback delegates for the Show methods expected
    /// for the item under test
    /// </summary>
    public Queue<Func<UICompletedEventArgs>> ShowResultResponders { get; set; }

    /// <summary>
    /// Queue of callback delegates for the ShowModally methods expected
    /// for the item under test
    /// </summary>
    public Queue<Func<UICompletedEventArgs>> ShowModallyResultResponders { get; set; }

    #endregion

    #region Ctor
    /// <summary>
    /// Ctor
    /// </summary>
    public TestChildWindowService()
    {
        ShowResultResponders = new Queue<Func<UICompletedEventArgs>>();
        ShowModallyResultResponders = new Queue<Func<UICompletedEventArgs>>();
    }
    #endregion

    #region ISLChildWindowService Members

    /// <summary>
    /// Registers a type through a key.
    /// </summary>
    /// <param name="key">Key for the UI dialog</param>
    /// <param name="winType">Type which implements dialog</param>
    public void Register(string key, Type winType)
    {
        //Nothing to do, as there will never be a UI
        //as we are testing the VMs
    }

    /// <summary>
    /// Does nothing, as nothing required for testing
    /// </summary>
    /// <param name="key">Key to remove</param>
    /// <returns>True/False success</returns>
    public bool Unregister(string key)
    {
        //Nothing to do, as there will never be a UI
        //as we are testing the VMs, simple return true
        return true;
    }

    /// <summary>
    /// This method displays a modaless dialog associated with the given key.
    /// </summary>
    /// <param name="key">Key previously registered with the UI controller.</param>
    /// <param name="state">Object state to associate with the dialog</param>
    /// <param name="completedProc">Callback used when UI closes (may be null), 
    /// which will have the Dialog state and result in it </param>
    public void Show(string key, object state,
        EventHandler<UICompletedEventArgs> completedProc)
    {

        if (ShowResultResponders.Count == 0)
            throw new Exception(
                "TestChildWindowService Show method expects " + 
                "a Func<UICompletedEventArgs> callback \r\n" +
                "delegate to be enqueued for each Show call");
        else
        {
            //Get and fire callback (defined in UnitTest)
            Func<UICompletedEventArgs> responder = ShowResultResponders.Dequeue();
            if (completedProc != null)
            {
                completedProc(null, responder());
            }
        }
    }

    /// <summary>
    /// This method displays a modal dialog associated with the given key.
    /// </summary>
    /// <param name="key">Key previously registered with the UI controller.</param>
    /// <param name="are">Wait handle to allow the popup to be shown Modally, and signal the
    /// blocked Waiting using code to continue</param>
    /// <param name="state">Object state to associate with the dialog</param>
    /// <param name="completedProc">Callback used when UI closes (may be null), 
    /// which will have the Dialog state and result in it </param>
    public void ShowModally(string key, object state, AutoResetEvent are,
        EventHandler<UICompletedEventArgs> completedProc)
    {
        if (ShowModallyResultResponders.Count == 0)
            throw new Exception(
                "TestChildWindowService Show method expects " + 
                "a Func<UICompletedEventArgs> callback \r\n" +
                "delegate to be enqueued for each Show call");
        else
        {
            //Get and fire callback (defined in UnitTest)
            Func<UICompletedEventArgs> responder = 
                           ShowModallyResultResponders.Dequeue();
            if (completedProc != null)
            {
                completedProc(null, responder());

                //As its Modal, signal the WaitHandle which
                //should be blocked waiting in calling code.
                //Tell it that it can now continue
                if (are != null)
                {
                    are.Set();
                }
            }
        }
    }
    #endregion
}

模态(阻塞)

正如我在上面提到的,Silverlight 中 `ChildWindow` 的默认行为是非阻塞的(大概是因为它的主要目的是在某个长时间运行的操作发生时显示状态信息……但人们终究是人,会以不同的方式使用它,我也是,而且会继续如此)。所以,如果我们想让 UI 阻塞等待 `ChildWindow`,我们需要做一些不同的事情。

不过,使用附带的 `SLChildWindowService` 仍然非常简单,您只需要提供额外的一条信息。

显示 `ChildWindow` 子类就像这样简单,我们显示弹出窗口并设置一个回调委托,当 `ChildWindow` 子类的 `Closed` 事件发生时,就会调用该回调委托,如上面所示的 Silverlight 实现中所见。

但也要注意,这次我们提供了一个 `AutoResetEvent WaitHandle`,允许调用代码阻塞等待,直到 `ChildWindow` 子类向此确切的 `AutoResetEvent WaitHandle` 发出信号,表明它可以继续。

AutoResetEvent are = new AutoResetEvent(false);
synContext.InvokeSynchronously(delegate()
{
    popupVisualizer.ShowModally("ImagePopup", this, are, (s, e) =>
    {
        if (!e.Result.Value)
        {
            msgbox.ShowInformation(
                "You clicked Cancel. So this could be used to " + 
                "delete the File from the WebServer.\r\n" +
                "Possibly on a new thread, Instead " + 
                "of showing this blocking Modal MessageBox");
        }
    });
});
are.WaitOne();

让我们再看一下实际 Silverlight `ISLChildWindowService` 实现的相关部分,看看这个 `AutoResetEvent WaitHandle` 是如何被信号化的。

//if there is a callback, call it on Closed
if (completedProc != null)
{
    win.Closed +=
        (s, e) =>
        {
            //if modal and there is a wait handle
            //associated signal it to continue
            if (isModal)
            {
                if (are != null)
                {
                    DoCallback(win, completedProc);
                    are.Set();
                }
                else
                {
                    DoCallback(win, completedProc);
                }
            }
            else
            {
                DoCallback(win, completedProc);
            }
        };
}

无模式/模态之间的测试设置差异

设置此服务的非模态和模态测试版本没有区别。

他人的工作

我应该指出,在我写完代码和文章文本后,我用 Google 快速搜索了一下,看看这是否是其他人的问题,并找到了这个:一个用于 MVVM 应用程序的 ChildWindow 管理服务,如果您使用 PRISM/Unity,这很棒。正如我所说,我是在完成后才找到的,我的服务非常不同,而且不依赖于任何 IOC 或 PRISM,但如果您使用这种组合,那篇文章相当有趣。可以看看。

线程辅助

我包含了几种线程助手。

Dispatcher 扩展方法

这些简单的 `Dispatcher` 扩展方法在处理 `Dispatcher` 时提供了便捷的语法。

public static class DispatcherExtensions
{

#if !SILVERLIGHT
   /// <summary>
   /// A simple threading extension method, to invoke a delegate
   /// on the correct thread if it is not currently on the correct thread
   /// which can be used with DispatcherObject types.
   /// </summary>
   /// <param name="dispatcher">The Dispatcher object on which to
   /// perform the Invoke</param>
   /// <param name="action">The delegate to run</param>
   /// <param name="priority">The DispatcherPriority for the invoke.</param>
   public static void InvokeIfRequired(this Dispatcher dispatcher,
           Action action, DispatcherPriority priority)
   {
       if (!dispatcher.CheckAccess())
       {
            dispatcher.Invoke(priority, action);
       }
       else
       {
            action();
       }
   }
#endif

    /// <summary>
    /// A simple threading extension method, to invoke a delegate
    /// on the correct thread if it is not currently on the correct thread
    /// which can be used with DispatcherObject types.
    /// </summary>
    /// <param name="dispatcher">The Dispatcher object on which to
    /// perform the Invoke</param>
    /// <param name="action">The delegate to run</param>
    public static void InvokeIfRequired(this Dispatcher dispatcher, Action action)
    {
        if (!dispatcher.CheckAccess())
        {
#if SILVERLIGHT
            dispatcher.BeginInvoke(action);
#else
            dispatcher.Invoke(DispatcherPriority.Normal, action);
#endif
        }
        else
        {
            action();
        }
    }

    /// <summary>
    /// A simple threading extension method, to invoke a delegate
    /// on the correct thread if it is not currently on the correct thread
    /// which can be used with DispatcherObject types.
    /// </summary>
    /// <param name="dispatcher">The Dispatcher object on which to
    /// perform the Invoke</param>
    /// <param name="action">The delegate to run</param>
    public static void InvokeInBackgroundIfRequired(this Dispatcher dispatcher, 
                                                    Action action)
    {
        if (!dispatcher.CheckAccess())
        {
#if SILVERLIGHT
            dispatcher.BeginInvoke(action);
#else
            dispatcher.Invoke(DispatcherPriority.Background, action);
#endif
        }
        else
        {
            action();
        }
    }

    /// <summary>
    /// A simple threading extension method, to invoke a delegate
    /// on the correct thread asynchronously if it is not currently on the correct thread
    /// which can be used with DispatcherObject types.
    /// </summary>
    /// <param name="dispatcher">The Dispatcher object on which to
    /// perform the Invoke</param>
    /// <param name="action">The delegate to run</param>
    public static void InvokeAsynchronouslyInBackground(this Dispatcher dispatcher, 
                                                        Action action)
    {
#if SILVERLIGHT
        dispatcher.BeginInvoke(action);
#else
                   dispatcher.BeginInvoke(DispatcherPriority.Background, action);
#endif
    }
}

这允许您执行类似这样的操作:

uiDispatcher.InvokeIfRequired(()=>
{
     //do something on UI
});

Dispatcher SynchronizationContext

当您使用 Windows Forms 时,有一个 `SynchronizationContext` 可以用来发送和发布委托。我在这个线程文章中讨论了这一点。为 WPF 创建一个 `SynchronizationContext` 是可能的,这正是我的好友 Daniel Vaughan 曾经做过的事情,我已将该代码包含在这个小型 SL MVVM 库中。基本上,它归结为这个 `UISynchronizationContext` 类。谢谢 Daniel。

public class UISynchronizationContext : ISynchronizationContext
{
    #region Data
    private DispatcherSynchronizationContext context;
    private Dispatcher dispatcher;
    private readonly object initializationLock = new object();
    #endregion

    #region Singleton implementation

    static readonly UISynchronizationContext instance = new UISynchronizationContext();

    /// <summary>
    /// Gets the singleton instance.
    /// </summary>
    /// <value>The singleton instance.</value>
    public static ISynchronizationContext Instance
    {
        get
        {
            return instance;
        }
    }

    #endregion

    #region Public Methods
    public void Initialize()
    {
        EnsureInitialized();
    }
    #endregion

    #region ISynchronizationContext Members

    public void Initialize(Dispatcher dispatcher)
    {
        ArgumentHelper.AssertNotNull(dispatcher, "dispatcher");

        lock (initializationLock)
        {
            this.dispatcher = dispatcher;
            context = new DispatcherSynchronizationContext(dispatcher);
        }
    }

    public void InvokeAsynchronously(SendOrPostCallback callback, object state)
    {
        ArgumentHelper.AssertNotNull(callback, "callback");
        EnsureInitialized();

        context.Post(callback, state);
    }

    public void InvokeAsynchronously(Action action)
    {
        ArgumentHelper.AssertNotNull(action, "action");
        EnsureInitialized();

        if (dispatcher.CheckAccess())
        {
            action();
        }
        else
        {
            dispatcher.BeginInvoke(action);
        }
    }

    public void InvokeSynchronously(SendOrPostCallback callback, object state)
    {
        ArgumentHelper.AssertNotNull(callback, "callback");
        EnsureInitialized();

        context.Send(callback, state);
    }

    public void InvokeSynchronously(Action action)
    {
        ArgumentHelper.AssertNotNull(action, "action");
        EnsureInitialized();

        if (dispatcher.CheckAccess())
        {
            action();
        }
        else
        {
            context.Send(delegate { action(); }, null);
        }
    }

    public bool InvokeRequired
    {
        get
        {
            EnsureInitialized();
            return !dispatcher.CheckAccess();
        }
    }

    #endregion

    #region Private Methods
    private void EnsureInitialized()
    {
        if (dispatcher != null && context != null)
        {
            return;
        }

        lock (initializationLock)
        {
            if (dispatcher != null && context != null)
            {
                return;
            }

            try
            {
        dispatcher = System.Windows.Deployment.Current.Dispatcher;
                context = new DispatcherSynchronizationContext(dispatcher);
            }
            catch (InvalidOperationException)
            {
                throw new Exception("Initialised called from non-UI thread.");
            }
        }
    }
    #endregion
}

然后,我们可以使用它来完成所有与 `Dispatcher` 相关的线程协调工作,例如:

synContext.InvokeSynchronously(delegate()
{
    FileMessage = "Total file size: " + FileSize + " Uploading: " +
        string.Format("{0:###.00}%", (double)dataSent / (double)dataLength * 100);
});

其中 `FileMessage` 属性在 XAML 中绑定,`FileMessage` 属性存在于 ViewModel 中,并且这在线程上下文中,所以我们必须确保 `FileMessage` 属性在 UI 的 `Dispatcher` 线程上更新。

它不做什么

对以下内容没有任何支持:

  • `IEditableObject` 对 ViewModel 的支持
  • `IDataErrorInfo` / 验证 / 业务规则
  • 数据包装器,如我在 Cinch WPF MVVM Framework 中找到的。
  • 响应 UI 事件(如 MouseDown)在 ViewModel 中运行 `ICommand`。这看起来是 Silverlight 中使用 *Microsoft.Windows.Interactivity.dll* 的标准用法,然后利用 `TargetedTriggerAction`。我在我的博客 http://sachabarber.net/?p=510 上讨论了这一点(尽管它是针对 WPF 的,它允许我们继承自 `ICommandSource`,而 Silverlight 不允许)。我相信有人可以很好地实现它,而且很可能已经有人这样做了。

以上所有内容都由我的 Cinch WPF MVVM Framework 涵盖,但这个应用程序花了大约两天时间编写,而我的 Cinch WPF MVVM Framework 花了很长时间才写好。所以需要有所取舍。

现在,这些都可以很容易地添加,因为 `IEditableObject/IDataErrorInfo` 已经是 Silverlight SDK 的一部分了(以前 `IDataErrorInfo` 是缺失的)。

演示应用程序详解

好了,现在我已经解释了包含在这个代码演示应用程序中的小型 Silverlight MVVM 框架。

特别感谢/致谢

我应该指出,这段代码使用了我在 John Mendezes 的博客上找到的一些代码:Silverlight 3 中的文件上传,John 说他从一个开源的 CodePlex 项目中获取了它:Silverlight Multi File Uploader v3.0

我只是想玩转 Silverlight 4,并尝试以一种良好的 MVVM 方式来实现,但我还需要上传图片作为我计划中的一个更大项目的一部分,所以我从 John Mendezes 的代码开始。

回到正题

我想我们应该回到实际的演示应用程序,如果您还记得的话,它允许用户从本地文件系统上传一个文件到远程 Web 服务器,如果文件是图片,它将显示在一个 `ChildWindow` 中。

嗯,信不信由你,在我们介绍了附带的小型 Silverlight MVVM 框架之后,它实际上归结为一个非常简单的对象模型。

我们有一个 View(`FileUpload`),它使用了一个 ViewModel(`ImageUploadViewModel`)。John 的原始代码都是代码隐藏,所以我为自己设定的原始任务是将其 MVVM 化。

让我们看看 MVVM 化的代码:

文件上传

如前所述,有一个 `ImageUploadViewModel`,它公开了一个 `ICommand`,Silverlight View `FileUpload` 如下使用它:

<Button Content="Upload File" Command="{Binding UploadCommand}" />

当用户单击此按钮时,将在 `ImageUploadViewModel` 中运行以下逻辑;请注意使用 `ISLOpenFileService` 服务来显示一个打开文件对话框。

public ImageUploadViewModel()
{
    ofd = GetService<ISLOpenFileService>();
}

private void ExecuteUploadCommand()
{
    ofd.Multiselect = false;

    if (ofd.ShowDialog() == true)
    {
        if (ofd.File != null)
            fileToUpload = ofd.File;

        IsBusy = true;
        this.UploadFile();
    }
}

当用户选择一个文件后,文件上传过程就开始了,它会调用 `UploadFile()` 方法,该方法如下所示:

private void UploadFile()
{
    FileSize = this.GetFileSize(fileToUpload.Length);
    this.StartUpload(fileToUpload);
}

它进而调用 `StartUpload()` 方法,该方法如下所示,该方法将被调用多次以分块上传文件:

private void StartUpload(FileInfo file)
{
    uploadedFile = file;
    fileStream = uploadedFile.OpenRead();
    dataLength = fileStream.Length;

    long dataToSend = dataLength - dataSent;
    bool isLastChunk = dataToSend <= ChunkSize;
    bool isFirstChunk = dataSent == 0;
    docType = imageFileExtensions.Contains(
                uploadedFile.Extension.ToLower()) ? 
                documentType.Image : documentType.Document;

    UriBuilder httpHandlerUrlBuilder = 
    new UriBuilder(string.Format("{0}/FileUpload.ashx",baseUri));
    httpHandlerUrlBuilder.Query = 
    string.Format("{5}file={0}&offset={1}&last={2}&first={3}&docType={4}", 
            uploadedFile.Name, dataSent, isLastChunk, isFirstChunk, docType, 
            string.IsNullOrEmpty(httpHandlerUrlBuilder.Query) ? "" : 
            httpHandlerUrlBuilder.Query.Remove(0, 1) + "&");

    HttpWebRequest webRequest = 
    (HttpWebRequest)WebRequest.Create(httpHandlerUrlBuilder.Uri);
    webRequest.Method = "POST";
    webRequest.BeginGetRequestStream(
    new AsyncCallback(WriteToStreamCallback), webRequest);
}

这次上传被定向到 Silverlight 托管 Web 应用程序(*TestSL4.Web*)中的一个标准 HttpHandler(*FileUpload.ashx*),该 Handler 实际上将文件保存到远程 Web 服务器的虚拟目录。这个对 HttpHandler(*FileUpload.ashx*)的调用是使用异步委托(BeginInvoke/EndInvoke)完成的,因此提供了一个 `AsyncCallback` 委托来指向 `WriteToStreamCallback()` 方法。

这是 HttpHandler 代码:

<%@ WebHandler Language="C#" Class="FileUpload" %>

using System;
using System.Web;
using System.IO;
using System.Web.Hosting;
using System.Diagnostics;

/// <summary>
/// Saves the posted content as a file to the local web file system. The request
/// is expected to come from the <see cref="TestSL4.ImageUploadViewModel">
/// ImageUploadViewModel</see>
/// ViewModel
/// </summary>
public class FileUpload : IHttpHandler {

    private HttpContext _httpContext;
    private string _tempExtension = "_temp";
    private string _fileName;
    private string _docType;
    private bool _lastChunk;
    private bool _firstChunk;
    private long _startByte;
    private StreamWriter _debugFileStreamWriter;
    private TextWriterTraceListener _debugListener;

    public void ProcessRequest(HttpContext context)
    {
        _httpContext = context;

        if (context.Request.InputStream.Length == 0)
            throw new ArgumentException("No file input");

        try
        {
            GetQueryStringParameters();

            string uploadFolder = GetUploadFolder();
            string tempFileName = _fileName + _tempExtension;

            if (_firstChunk)
            {
                if (!Directory.Exists(
                        @HostingEnvironment.ApplicationPhysicalPath + "/" + uploadFolder))
                    Directory.CreateDirectory(
                        @HostingEnvironment.ApplicationPhysicalPath + "/" + uploadFolder);
                
                
                //Delete temp file
                if (File.Exists(@HostingEnvironment.ApplicationPhysicalPath + 
                    "/" + uploadFolder + "/" + tempFileName))
                    File.Delete(@HostingEnvironment.ApplicationPhysicalPath + 
                        "/" + uploadFolder + "/" + tempFileName);

                //Delete target file
                if (File.Exists(@HostingEnvironment.ApplicationPhysicalPath + 
                    "/" + uploadFolder + "/" + _fileName))
                    File.Delete(@HostingEnvironment.ApplicationPhysicalPath + 
                        "/" + uploadFolder + "/" + _fileName);

            }

            using (FileStream fs = File.Open(@HostingEnvironment.ApplicationPhysicalPath + 
                "/" + uploadFolder + "/" + tempFileName, FileMode.Append))
            {
                SaveFile(context.Request.InputStream, fs);
                fs.Close();
            }

            if (_lastChunk)
            {
                File.Move(HostingEnvironment.ApplicationPhysicalPath + "/" + uploadFolder + 
                    "/" + tempFileName, HostingEnvironment.ApplicationPhysicalPath + "/" + 
                    uploadFolder + "/" + _fileName);
            }
        }
        catch (Exception e)
        {
            throw;
        }
    }

    private void GetQueryStringParameters()
    {
        _fileName = _httpContext.Request.QueryString["file"];
        _docType = _httpContext.Request.QueryString["docType"];
        
        _lastChunk = string.IsNullOrEmpty(_httpContext.Request.QueryString["last"]) 
            ? true : bool.Parse(_httpContext.Request.QueryString["last"]);
        
        _firstChunk = string.IsNullOrEmpty(_httpContext.Request.QueryString["first"]) 
            ? true : bool.Parse(_httpContext.Request.QueryString["first"]);
        
        _startByte = string.IsNullOrEmpty(_httpContext.Request.QueryString["offset"]) 
            ? 0 : long.Parse(_httpContext.Request.QueryString["offset"]); ;
    }

    
    private void SaveFile(Stream stream, FileStream fs)
    {
        byte[] buffer = new byte[4096];
        int bytesRead;
        while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) != 0)
        {
            fs.Write(buffer, 0, bytesRead);
        }
    }


    protected string GetUploadFolder()
    {
        string folder = ""; 

        switch (_docType)
        {
            case "Document":
                folder = "documents/uploads";
                break;
            case "Image":
                folder = "documents/images";
                break;
            default:
                folder = "documents";
                break;
        }
        
        if (string.IsNullOrEmpty(folder))
            folder = "documents";

        return folder;
    }

    public bool IsReusable {
        get {
            return false;
        }
    }
}

您还可以注意到,提供了一个 `AsyncCallback` 委托来指向 `WriteToStreamCallback()` 方法。

private void WriteToStreamCallback(IAsyncResult asynchronousResult)
{
    HttpWebRequest webRequest = (HttpWebRequest)asynchronousResult.AsyncState;
    Stream requestStream = webRequest.EndGetRequestStream(asynchronousResult);

    byte[] buffer = new Byte[4096];
    int bytesRead = 0;
    int tempTotal = 0;

    //Set the start position
    fileStream.Position = dataSent;

    //Read the next chunk
    while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) 
    != 0 && tempTotal + bytesRead < ChunkSize)
    {
        requestStream.Write(buffer, 0, bytesRead);
        requestStream.Flush();

        dataSent += bytesRead;
        tempTotal += bytesRead;

        ////Show the progress change
        UpdateShowProgress(false);
    }

    requestStream.Close();

    //Get the response from the HttpHandler
    webRequest.BeginGetResponse(
    new AsyncCallback(ReadHttpResponseCallback), webRequest);

}

文件完全上传后显示 ChildWindow

由于我们想知道文件上传是否成功,HttpHandler 的响应也被用于一个 `AsyncCallback` 委托,该委托指向 `ReadHttpResponseCallback()` 方法。在 `ReadHttpResponseCallback()` 方法中,通过调用 `UpdateShowProgress()` 方法来更新 UI 状态,并检查 HttpHandler(*FileUpload.ashx*)流的结果,如果尚未发送整个原始文件字节,将再次调用 `StartUpload()` 来上传下一个块。

private void ReadHttpResponseCallback(IAsyncResult asynchronousResult)
{
    try
    {
        HttpWebRequest webRequest = 
                 (HttpWebRequest)asynchronousResult.AsyncState;
        HttpWebResponse webResponse = 
                 (HttpWebResponse)webRequest.EndGetResponse(asynchronousResult);
        StreamReader reader = new StreamReader(webResponse.GetResponseStream());

        string responsestring = reader.ReadToEnd();
        reader.Close();
    }
    catch
    {
        synContext.InvokeSynchronously(delegate()
        {
            ShowError();
        });
    }

    if (dataSent < dataLength)
    {
        //continue uploading the rest of the file in chunks
        StartUpload(uploadedFile);

        //Show the progress change
        UpdateShowProgress(false);
    }
    else
    {
        fileStream.Close();
        fileStream.Dispose();

        //Show the progress change
        UpdateShowProgress(true);
    }

}

如果已上传完整文件内容,文件将显示在 `ChildWindow` 中,如下所示:

private void UpdateShowProgress(bool complete)
{
    if (complete)
    {
        synContext.InvokeSynchronously(delegate()
        {
            FileMessage = "complete";
        });

        if (docType == documentType.Image)
        {
            AutoResetEvent are = new AutoResetEvent(false);
            synContext.InvokeSynchronously(delegate()
            {
                FinalFilePath = String.Format(@"{0}/{1}/{2}", 
                   baseUri, GetUploadFolder(), uploadedFile.Name);
                uploadedFile = null;
                IsBusy = false;

                popupVisualizer.ShowModally("ImagePopup", this, are, (s, e) =>
                {
                    if (!e.Result.Value)
                    {
                        msgbox.ShowInformation(
                            "You clicked Cancel. So this could be used " + 
                            "to delete the File from the WebServer.\r\n" +
                            "Possibly on a new thread, " + 
                            "Instead of showing this blocking Modal MessageBox");
                    }
                });
            });
            are.WaitOne();
        }
        else
        {
            synContext.InvokeSynchronously(delegate()
            {
                uploadedFile = null;
                IsBusy = false;
            });
        }

        //RESET ALL VALUES TO THEY CAN UPLOAD AGAIN
        fileToUpload = null;
        dataSent = 0;
        dataLength = 0;
        fileStream = null;
        FileSize = "";
    }
    else
    {
        synContext.InvokeSynchronously(delegate()
        {
            FileMessage = "Total file size: " + FileSize + 
                " Uploading: " +
                string.Format("{0:###.00}%", 
                   (double)dataSent / (double)dataLength * 100);
        });
    }
}

就是这样。希望您喜欢!

一如既往,欢迎投票/评论。

© . All rights reserved.