Silverlight 4 拖放文件管理器





5.00/5 (17投票s)
一个 Silverlight 文件管理器,允许拖放多个文件上传。
一个 Silverlight 4 拖放文件管理器
这是文章 Silverlight 文件管理器的第二部分。上一篇文章侧重于视图模型样式模式,以及它如何支持在Microsoft Expression Blend 4(或更高版本)中使用时的设计师/开发人员协作和工作流程。这一次,我们将通过实现拖放和实时文件上传进度通知来涵盖一些“棘手”的部分。然而,实现这一点将出奇地容易。
视图模型样式模式允许程序员创建一个完全没有 UI(用户界面)的应用程序。程序员只需要创建 **ViewModel** 和 **Model**。然后,没有任何编程能力的设计师可以从一张白纸开始,在 **Microsoft Expression Blend 4**(或更高版本)中完全创建 **View**(UI)。如果您是视图模型样式的新手,建议您阅读 Silverlight 视图模型样式:一种(过于)简化的解释 以获得入门介绍。
我们还将演示如何通过将 UI 元素传递给 **ViewModel** 来轻松解决任何编程挑战。通过根据需要连接 UI 元素的事件,您可以实现完全的 Microsoft Expression Blendability(在没有代码的情况下使用 Expression Blend 创建 UI 的能力)。
接力 Alan Beasley 的工作
该项目接续 Alan Beasley 的文章:在 Expression Blend & Silverlight 中进行列表框样式设计(第二部分 - 控件模板)。那篇文章又接续 Silverlight 视图模型样式文件管理器。在 Silverlight 视图模型样式文件管理器 中,文件管理器看起来是这样的:
在他撰写 在 Expression Blend & Silverlight 中进行列表框样式设计(第二部分 - 控件模板) 文章后,文件管理器看起来是这样的:
正如您所见,这不仅仅是“轻微”的改进。他完全没有触碰代码就做到了这一点,而且他可以自由地更改任何他想要的 UI 元素。
设计师:离不开他们……
将 UI 移交给设计师是有代价的。他们会像程序员保护自己的代码一样保护自己的设计。这个项目实际上经历了 Alan 和我的许多更改。我们达成了一个简单的协议。我不会触碰任何扩展名为“**.xaml**”(UI 和样式元素)的文件,他也不会触碰任何扩展名为“**.cs**”(代码)的文件。
在过去的几周里,我们已经证明了这确实有效。视图模型样式让我们能够实现这种“关注点分离”。他可以把 .xaml 文件通过电子邮件发给我,我只需将其放入我的项目版本中,然后“哦,设计变了”。
这是我们最近一次我“越界”的交流,当时我以为我在“帮忙”而自己为 UI 元素应用了样式:
我:“你的评论‘你创造的那个怪物……’让我哈哈大笑。因为你说得对!一开始我以为还可以,直到你说了之后。”
Alan:“你可能会笑,但我今天早上看到的时候几乎要死了!不!!!!!”
拖放文件管理器
本文介绍了文件上传功能的添加。它提供了通过简单地将文件拖放到文件夹详细信息面板(应用程序的右侧)来上传文件的能力。这是您查看所选文件夹中的文件时文件夹详细信息的显示方式:
当您将文件拖到面板上时,它会改变颜色。
当您将文件拖放到面板上时,会显示一个进度条。
上传过程完成后,将立即显示上传的文件。您只需单击一个文件即可下载它。
设计师:实现拖放
程序员会修改代码(任何除 **.xaml** 文件之外的文件),并将更新后的文件提供给设计师。如果您有源代码控制,您只需签入更改,设计师就会签出它们。请注意,Expression Blend 支持 Team Foundation Server 源代码控制。
要实现上传功能的 UI,设计师会在 **Microsoft Expression Blend 4**(或更高版本)中打开项目,并实现以下功能:
- 指示要用作上传文件的“放置点”的元素。
- 指示将用作“上传指示器”的控件。
“放置点”元素
设计师获取一个 **InvokeCommand** 行为……
……并将其从 **Objects and Timeline** 窗口中的 **LayoutRoot** 下拖动。
在行为的 **Properties** 中,设计师将 **EventName** 设置为 **Loaded**。这意味着当 **LayoutRoot** 首次加载时,该行为将被触发。此事件对于“进行设置”很有用,例如注册“放置点”。
接下来,设计师选择 **Command** 旁边的 **DataBind** 按钮。
**Command** 绑定到 **SetDropUIElementCommand**。
请注意,这是实现“简化的视图模型样式”的常用技术。我们需要挂接到该元素的鼠标悬停和放置事件。这通常很容易在代码隐藏中实现。使用视图模型样式,您不想使用代码隐藏(因为设计师无法轻松使用 Blend 来修改设计而不触碰代码)。因此,我们只需在应用程序首次加载时通过行为传递元素作为参数来“注册”该元素。 **SetDropUIElementCommand** 方法会连接我们需要的事件的处理程序。
有人会认为这会将 ViewModel 与 View 绑定在一起。然而,我们只是在传递一个参数。接受参数的 **SetDropUIElementCommand** 并不知道确切将用作放置点的元素。在这种情况下,参数类型是 UIElement。View 按定义由 UIElements 组成。设计师可以先使用 ScrollBox,稍后将其更改为矩形,而无需更改代码。
要指示将用作放置点的元素,请单击 **CommandParameter** 旁边的 **Advanced options** 按钮。
将 **FileDetails** (ScrollViewer) 指定为参数。
文件上传进度指示器
设计师获取一个 **BusyIndicator** 控件……
……并将其控件的 **IsBusy** 属性绑定到 **FileUploadingProperty**,将 **BusyContent** 属性绑定到 **FileuploadPercentProperty**。
设计师按下 **F5** 运行项目,应用程序即告完成!
代码
程序员的工作更复杂一些,以下是我需要做出的基本更改:
- Silverlight 代码
- 允许设计师指定要用作文件上传放置点的 UIElement。
- 更改 UIElement 的不透明度以指示放置点。
- 启动文件上传,并连接事件。
- 上传进度。
- 更新 FileUploadingProperty。
- 更新 FileUploadPercentProperty。
- 上传完成。
- 刷新文件文件夹。
- 上传进度。
- 允许设计师指定要用作文件上传放置点的 UIElement。
- 网站代码
- 上传代码(当 FileUpload 类调用网站中的 .ashx 文件时)。
文件上传器代码
对于文件上传,我决定使用来自 http://silverlightfileupld.codeplex.com 的 **Silverlight 文件上传** 项目的代码(作者:darick_c)。我之前已经用过这个代码几次,用于 **DNN Silverlight** 的模块。在原始项目中,他有一个功能齐全的上传控件。此上传客户端的管理占据了原始解决方案中的大部分代码。
我的目标是减少代码,只保留上传文件所需的元素,并对其进行重构,使其能够与视图模型样式模式配合使用。
用作放置点的元素
以下是允许设计师注册 **UIElement** 作为放置点的完整代码。
#region SetDropUIElementCommand public ICommand SetDropUIElementCommand { get; set; } public void SetDropUIElement(object param) { // Set the UI Element to be the drop point for files DropUIElement = (UIElement)param; // Save the opacity of the element DropUIElementOpacity = DropUIElement.Opacity; // Turn on allow drop DropUIElement.AllowDrop = true; // Attach event handlers DropUIElement.DragOver += new DragEventHandler(DropUIElement_DragOver); DropUIElement.DragLeave += new DragEventHandler(DropUIElement_DragLeave); DropUIElement.Drop += new DragEventHandler(DropUIElement_Drop); } private bool CanSetDropUIElement(object param) { return true; } void DropUIElement_DragOver(object sender, DragEventArgs e) { // Only allow drop if not uploading if (FileUploadingProperty == false) { // If you hover over the drop point, change it's opacity so users will // have some indication that they can drop DropUIElement.Opacity = (double)0.5; } } void DropUIElement_DragLeave(object sender, DragEventArgs e) { // Return opacity to normal DropUIElement.Opacity = DropUIElementOpacity; } #endregion
它为元素附加了 3 个处理程序:
- DragOver - 调用 **DropUIElement_DragOver**,它将不透明度更改为 0.5,以便用户知道这是一个放置点。
- DragLeave - 调用 **DropUIElement_DragLeave**,它将不透明度恢复正常。
- Drop - 调用 **DropUIElement_Drop**,然后调用方法上传文件。
这是 **DropUIElement_Drop** 的代码:
#region DropUIElement_Drop void DropUIElement_Drop(object sender, DragEventArgs e) { // Only allow drop if not uploading if (FileUploadingProperty == false) { // Return opacity to normal DropUIElement.Opacity = DropUIElementOpacity; // If there is something being dropped upload it if (e.Data != null) { FileInfo[] Dropfiles = e.Data.GetData(DataFormats.FileDrop) as FileInfo[]; files = new ObservableCollection(); // Get the upload URL string strURLWithSelectedFolder = string.Format("{0}?folder={1}", GetWebserviceAddress(), SelectedSilverlightFolder.FullPath); Uri uri = new Uri(strURLWithSelectedFolder, UriKind.Absolute); UploadUrl = uri; foreach (FileInfo fi in Dropfiles) { // Create an FileUpload object FileUpload upload = new FileUpload(App.Current.RootVisual.Dispatcher, UploadUrl, fi); if (UploadChunkSize > 0) { upload.ChunkSize = UploadChunkSize; } if (MaximumTotalUpload >= 0 && TotalUploadSize + upload.FileLength > MaximumTotalUpload) { MessageBox.Show("You have exceeded the total allowable upload amount."); break; } if (MaximumUpload >= 0 && upload.FileLength > MaximumUpload) { MessageBox.Show(string.Format("The file '{0}' exceeds the maximun upload size.", upload.Name)); break; } // Wire up handles for status changed and upload percentage // These will be updating the properties that the ViewModel exposes upload.StatusChanged += new EventHandler(upload_StatusChanged); upload.UploadProgressChanged += new ProgressChangedEvent(upload_UploadProgressChanged); // Start the Upload upload.Upload(); } } } } #endregion
此代码执行以下操作:
- 获取用于上传文件的 URL。
- 调用 **FileUpload** 类,传递:
- App.Current.RootVisual.Dispatcher - 应用程序的当前 Dispatcher。 **FileUpload** 对象需要 Dispatcher 来连接委托。
- UploadUrl - 要上传当前文件的 URL。
- fi - 要上传的当前文件。
它还连接了以下事件:
- upload.StatusChanged - 调用 **upload_StatusChanged** 来跟踪文件上传何时完成。
- upload.UploadProgressChanged - 调用 **upload_UploadProgressChanged** 来跟踪上传进度。
当 **FileUpload** 状态改变时(例如完成时),会触发以下方法:
#region upload_StatusChanged void upload_StatusChanged(object sender, EventArgs e) { FileUpload fu = sender as FileUpload; FileUploadingProperty = (fu.Status == FileUploadStatus.Uploading); if (fu.Status == FileUploadStatus.Complete) { // Refresh files for the selected folder SetFiles(SelectedSilverlightFolder); } } #endregion
如果上传完成,则调用 **SetFiles(SelectedSilverlightFolder)** 来刷新当前文件列表,并导致上传的文件显示在文件列表中。
文件上传类
此类由 **darick_c** 创建的原始类。我只是对其进行了大量精简。由于该类不再需要管理原始项目的 UI,因此仍然有许多代码可以被进一步精简。但是,我已经将其精简到足以让其他人能够相对容易地将其改编用于自己的用途。
在 **Uploader** 类中,这是执行大部分繁重工作的代码:
public void UploadFileEx() { Status = FileUploadStatus.Uploading; long temp = FileLength - BytesUploaded; UriBuilder ub = new UriBuilder(UploadUrl); bool complete = temp <= ChunkSize; ub.Query = string.Format("{3}filename={0}&StartByte={1}&Complete={2}", File.Name, BytesUploaded, complete, string.IsNullOrEmpty(ub.Query) ? "" : ub.Query.Remove(0, 1) + "&"); HttpWebRequest webrequest = (HttpWebRequest)WebRequest.Create(ub.Uri); webrequest.Method = "POST"; webrequest.BeginGetRequestStream(new AsyncCallback(WriteCallback), webrequest); } private void WriteCallback(IAsyncResult asynchronousResult) { HttpWebRequest webrequest = (HttpWebRequest)asynchronousResult.AsyncState; // End the operation. Stream requestStream = webrequest.EndGetRequestStream(asynchronousResult); byte[] buffer = new Byte[4096]; int bytesRead = 0; int tempTotal = 0; Stream fileStream = resizeStream != null ? (Stream)resizeStream : File.OpenRead(); fileStream.Position = BytesUploaded; while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0 && tempTotal + bytesRead < ChunkSize && !cancel) { requestStream.Write(buffer, 0, bytesRead); requestStream.Flush(); BytesUploaded += bytesRead; tempTotal += bytesRead; if (UploadProgressChanged != null) { int percent = (int)(((double)BytesUploaded / (double)FileLength) * 100); UploadProgressChangedEventArgs args = new UploadProgressChangedEventArgs(percent, bytesRead, BytesUploaded, FileLength, file.Name); this.Dispatcher.BeginInvoke(delegate() { UploadProgressChanged(this, args); }); } } // only close the stream if it came from the file, don't close resizestream so we don't have to resize it over again. if (resizeStream == null) fileStream.Close(); requestStream.Close(); webrequest.BeginGetResponse(new AsyncCallback(ReadCallback), webrequest); } private void ReadCallback(IAsyncResult asynchronousResult) { HttpWebRequest webrequest = (HttpWebRequest)asynchronousResult.AsyncState; HttpWebResponse response = (HttpWebResponse)webrequest.EndGetResponse(asynchronousResult); StreamReader reader = new StreamReader(response.GetResponseStream()); string responsestring = reader.ReadToEnd(); reader.Close(); if (cancel) { if (resizeStream != null) resizeStream.Close(); if (remove) Status = FileUploadStatus.Removed; else Status = FileUploadStatus.Canceled; } else if (BytesUploaded < FileLength) UploadFileEx(); else { if (resizeStream != null) resizeStream.Close(); Status = FileUploadStatus.Complete; } }
基本上,它是一块一块地上传文件。对这个过程的精心管理是这个类的美妙之处。所有功劳都完全属于 **darick_c**。如果它有任何问题,肯定是我引入的。还有其他上传文件的方法,但我已经使用了这个代码很多年,并且在许多不同的项目上使用过,这个代码对我来说效果很好。
Web 服务器代码
Web 服务器代码更直接。基本上,它与 **FileUpload** 类进行通信。其中大部分代码来自原始的 **darick_c** 项目。同样,我对其进行了简化,使其更易于定制。服务器上 .ascx 文件的代码如下:
private HttpContext ctx; public void ProcessRequest(HttpContext context) { ctx = context; FileUploadProcess fileUpload = new FileUploadProcess(); fileUpload.FileUploadCompleted += new FileUploadCompletedEvent(fileUpload_FileUploadCompleted); fileUpload.ProcessRequest(context); } void fileUpload_FileUploadCompleted(object sender, FileUploadCompletedEventArgs args) { string id = ctx.Request.QueryString["id"]; } public bool IsReusable { get { return false; } }
该代码调用 **ProcessRequest** 来处理上传。
#region class FileUploadProcess public class FileUploadProcess { public event FileUploadCompletedEvent FileUploadCompleted; #region ProcessRequest public void ProcessRequest(HttpContext context) { // ** Selected Folder is passed in the Header string strfolder = context.Request.QueryString["folder"]; // Other values passed string Originalfilename = string.IsNullOrEmpty(context.Request.QueryString["filename"]) ? "Unknown" : context.Request.QueryString["filename"]; bool complete = string.IsNullOrEmpty(context.Request.QueryString["Complete"]) ? true : bool.Parse(context.Request.QueryString["Complete"]); bool getBytes = string.IsNullOrEmpty(context.Request.QueryString["GetBytes"]) ? false : bool.Parse(context.Request.QueryString["GetBytes"]); long startByte = string.IsNullOrEmpty(context.Request.QueryString["StartByte"]) ? 0 : long.Parse(context.Request.QueryString["StartByte"]); ; string strExtension = System.IO.Path.GetExtension(Originalfilename); string strFileDirectory = context.ApplicationInstance.Server.MapPath(@"~\Files\"); strFileDirectory = strFileDirectory + @"\" + strfolder; string filePath = Path.Combine(strFileDirectory, Originalfilename); if (getBytes) { FileInfo fi = new FileInfo(filePath); // If file exists - delete it if (fi.Exists) { try { fi.Delete(); } catch { // could not delete } } context.Response.Write("0"); context.Response.Flush(); return; } else { if (startByte > 0 && File.Exists(filePath)) { using (FileStream fs = File.Open(filePath, FileMode.Append)) { SaveFile(context.Request.InputStream, fs); fs.Close(); } } else { using (FileStream fs = File.Create(filePath)) { SaveFile(context.Request.InputStream, fs); fs.Close(); } } if (complete) { if (FileUploadCompleted != null) { FileUploadCompletedEventArgs args = new FileUploadCompletedEventArgs(Originalfilename, filePath); FileUploadCompleted(this, args); } } } } #endregion #region SaveFile 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); } } #endregion } #endregion
视图模型样式简化
通过使用 **ICommands** 将 UI 元素传递给 **ViewModel** 并根据需要将事件连接到这些元素,您可以实现完全的 Microsoft Expression **Blendability** 和测试。想测试一个需要 **UIElement** 作为参数的 **ICommand**?将一个 **UIElement** 传递给它,在您的测试方法中触发事件,然后比较预期结果。
当您将 UI 元素作为参数传递时,您可以连接任何事件,例如 **MouseDown**。这允许您响应直接操作,而不是试图纯粹通过绑定来推断 View 中的意图。虽然这看起来像是将您的 **ViewModel** 与您的 **View** 绑定在一起,但实际传递的 UI 元素可以轻松地由设计师更改。通过这种方式,您可以为设计师提供最大的灵活性,让他们真正“设计”应用程序,而不仅仅是“样式化”它。