分析Silverlight文件上传组件

  文件上传是日常开过程中最常用的功能之一,目前实现文件上传的方式多种多样。这其中较为复杂的情况就是关于大文件、多文件上传的问题,目前解决大文件、多文件上传一般借助于js或者flash组件,今天就同大家一起看一下如何使用silverlight实现这个功能,而且功能和用户体验相对会更好一些。

10多年的轵城网站建设经验,针对设计、前端、开发、售后、文案、推广等六对一服务,响应快,48小时及时工作处理。成都全网营销推广的优势是能够根据用户设备显示端的尺寸不同,自动调整轵城建站的显示方式,使网站能够适用不同显示终端,在浏览器中调整网站的宽度,无论在任何一种浏览器上浏览网站,都能展现优雅布局与设计,从而大程度地提升浏览体验。创新互联从事“轵城网站设计”,“轵城网站推广”以来,每个客户项目都认真落实执行。

  主要内容:

  一、组件特点

  二、实现原理

  三、编码实现

  一、组件特点

  对于今天要说的组件姑且叫做"CmjUpload"吧,方便称呼。目前有很多上传组件来辅助完成日常开发,"CmjUpload"有什么特点呢:

  解决大文件、多文件上传问题

  基于asp.net上传,不需要部署WCF、WebService操作方便

  接口丰富、灵活性强,配置使用方便。

  支持选择、拖拽两种文件添加方式上传,用户体验好。

  支持取消、暂停、继续操作满足续传要求。

  OK,就说那么多吧,主要是让大家有兴趣看下去,其实之所以有今天的话题主要还是为了学习以及满足实际开发需求。

  二、实现原理

  在Silverlight中要实现上传有很多方式,例如说使用WCF或者WebService,但是考虑到实际情况,这里没有选择以上两种方式,而是选择了WebRequest方式。原因比较简单,部署十分方便,不需要为了上传组件而进行额外的配置。Silverlight中使用WebRequest同其他.Net开发中的使用方式是类似的,不同的是Silverlight中很多操作都是异步的,当然WebRequest也不例外。此外,在这里需要对一个文件分块发送,一方面可以解决大文件上传问题,另一方面可以实时显示文件上传进度。下面一个简单的交互过程:

  当然要完成整个组件远不止上面说的这些,UI的设计,组件的本地化,用户接口的设计等都是必须思考的问题。下面是组件界面原型:

  界面分为两个区域:文件显示区域和操作区域,当然这里的文件区域本身也是可以操作的,例如如果你不想点击按钮选择文件的话,可以选择直接拖拽一个或多个文件到文件区域。还可以对已添加的文件进行删除操作,对正在上传的文件进行暂停和续传操作。此外文件区域的设计主要提供文件信息显示,例如缩略图、上传进度、文件名称、文件大小等信息。操作区域一方面提供文件整体信息的显示(例如文件总数、已上传数等),另一方面提供了文件浏览、上传、清空操作。

  下面是类的设计:

在上图中我们可以看出有三个包:Core、Config、Util。

  Core是核心包,里面主要包括文件队列管理(FileQueue)、文件上传控制(FileUpload)、文件界面区域(FileArea)、文件大小单位转换(FileSize)、缩略图控制(FileIcon)。

  Config是配置和接口包,主要包括组件设计级别常量(注意不是用户级别也不是开发级别,开发级别配置在接口中进行)(UploadConstant)、客户端开发接口(ExposeInterface)、本地化实现(Localization)、接口注册(ClientInteraction)。

  Util包主要包括一些常用辅助类,主要包括xml操作(XmlHelper)、服务器端文件保存辅助类(CmjUpload)。

  三、编码实现

  有了上面的分析相信下面的实现就相当容易理解了,首先看一下文件上传类FileUpload:

  using System;

  using System.Net;

  using System.Windows;

  using System.Windows.Controls;

  using System.Windows.Documents;

  using System.Windows.Ink;

  using System.Windows.Input;

  using System.Windows.Media;

  using System.Windows.Media.Animation;

  using System.Windows.Shapes;

  using System.Text;

  using System.IO;

  using System.Windows.Threading;

  using CmjUpload.Util;

  using CmjUpload.Config;

  namespace CmjUpload

  {

  public class FileUpload

  {

  //开始上传

  public delegate void StartUploadHanler(object sender,EventArgs e);

  public event StartUploadHanler StartUpload;

  public void OnStartUpload(object sender, EventArgs e)

  {

  if (StartUpload != null)

  {

  StartUpload(sender, e);

  }

  }

  // 上传

  public delegate void UploadingHanler(object sender, ProgressArgs e);

  public event UploadingHanler Uploading;

  public void OnUploading(object sender, ProgressArgs e)

  {

  if (Uploading != null)

  {

  Uploading(sender,e);

  }

  }

  //上传结束

  public delegate void UploadCompletedHanler(object sender, EventArgs e);

  public event UploadCompletedHanler UploadCompleted;

  public void OnUploadCompleted(object sender, EventArgs e)

  {

  if (UploadCompleted != null)

  {

  UploadCompleted(sender, e);

  }

  }

  private string _requestUrl = "";

  private string _fileName = "";

  private long _fileLength = 0;

  private long _blockLength = 4096;//单次上传文件大小

  private long _postedLength = 0;//已传输文件大小

  private long _nextLength = 0;//下次传输的文件大小

  private bool _firstUpload = true;

  private BinaryReader _fileReader = null;

  private UploadStatus _uploadStatus = UploadStatus.Start;

  public FileInfo File

  {

  get;

  set;

  }

  //public long PostedLength

  //{

  // get

  // {

  // return _postedLength;

  // }

  // set

  // {

  // _postedLength = value;

  // }

  //}

  public UploadStatus Status

  {

  get

  {

  return _uploadStatus;

  }

  set

  {

  _uploadStatus = value;

  }

  }

  public void Upload(FileInfo file)

  {

  this.File = file;

  //XmlHelper xmlHelper = new XmlHelper("Config/CmjUploadConfig.xml");

  //_requestUrl=xmlHelper.GetAttibuteValue("Upload", "RequestUrl");

  _requestUrl = ExposeInterface.Instance().RequestUrl;

  this._fileName = this.File.Name;

  this._fileLength = this.File.Length;

  this._blockLength = FileSize.GetLockSize(this._fileLength);

  //this._postedLength = 0;

  _fileReader = new BinaryReader(file.OpenRead());

  //_uploadStatus = UploadStatus.Start;

  if (_fileLength <_blockLength)

  {

  _nextLength = _fileLength;

  }

  else

  {

  _nextLength = _blockLength;

  }

  OnStartUpload(this, new EventArgs());

  UploadInBlock();

  }

  public void UploadInBlock()//上传一块数据

  {

  UriBuilder uriBuilder = new UriBuilder(new Uri(_requestUrl, UriKind.Absolute));

  uriBuilder.Query = string.Format("fileName={0}&status="+_uploadStatus,this._fileName);

  WebRequest request = WebRequest.Create(uriBuilder.Uri);

  request.Method = "POST";

  request.ContentType = "multipart/mixed";//注意这里

  request.ContentLength = _nextLength;

  if (_firstUpload)

  {

  _uploadStatus = UploadStatus.Uploading;

  _firstUpload = false;

  }

  request.BeginGetRequestStream((IAsyncResult asyncResult) =>

  {

  WebRequest rqst = asyncResult.AsyncState as WebRequest;

  Stream rqstStm = rqst.EndGetRequestStream(asyncResult);

  byte[] buffer = new byte[_blockLength];

  int size = _fileReader.Read(buffer, 0, buffer.Length);

  if(size>0)

  {

  rqstStm.Write(buffer, 0, size);

  rqstStm.Flush();

  _postedLength += size;

  if ((_fileLength - _postedLength) <_blockLength)

  {

  _nextLength = _fileLength-_postedLength;

  }

  }

  rqstStm.Close();

  rqst.BeginGetResponse((IAsyncResult ascResult) =>//开始数据传输

  {

  OnUploading(this, new ProgressArgs() { Percent = ((double)_postedLength / (double)_fileLength) });

  WebRequest webRequest = ascResult.AsyncState as WebRequest;

  WebResponse webResponse = (WebResponse)webRequest.EndGetResponse(ascResult);

  StreamReader reader = new StreamReader(webResponse.GetResponseStream());

  string responsestring = reader.ReadToEnd();

  reader.Close();

  if (_postedLength >= _fileLength)

  {

  _uploadStatus = UploadStatus.Complelte;

  }

  if (_uploadStatus == UploadStatus.Uploading)

  {

  UploadInBlock();

  }

  //else if(_uploadStatus==UploadStatus.Cancel)

  //{

  // return;

  //}

  else if (_uploadStatus==UploadStatus.Complelte)

  {

  _fileReader.Close();

  OnUploadCompleted(this, new EventArgs());

  }

  }, request);

  }, request);

  }

  ///

  /// 继续上传

  ///

  ///

  ///

  //public static void ContinueUplaod(string fileName,long uploadedLength)

  //{

  //}

  }

  //上传进度参数

  public class ProgressArgs:EventArgs

  {

  public double Percent

  {

  get;

  set;

  }

  }

  public enum UploadStatus

  {

  Start,

  Uploading,

  Cancel,

  Complelte

  }

  }

  在这个类中需要注意的是状态的控制,因为组件需要实现文件暂停、续传功能,并且每次请求时需要发送相应的操作状态;另一点就是对外公开了三个事件,用于给UI提供进度支持和状态通知。

  FileQueue管理整个文件队列,控制着界面UI、文件上传等信息:

  using System;

  using System.Collections.Generic;

  using System.IO;

  namespace CmjUpload

  {

  ///

  /// 文件队列管理者

  ///

  public class FileQueue

  {

  private static object _lock = new object();

  private static FileQueue _fileQueue = null;

  private Dictionary _fileIndexs = null;//文件同索引对应关系

  private Dictionary _files = null;

  private Dictionary _fileAeas = null;

  private Dictionary _fileUploader = null;

  private int index = 0;

  private FileQueue()

  {

  _fileIndexs = new Dictionary();

  _files = new Dictionary();

  _fileAeas = new Dictionary();

  _fileUploader = new Dictionary();

  }

  public static FileQueue Instance()

  {

  lock (_lock)

  {

  if (_fileQueue == null)

  {

  _fileQueue = new FileQueue();

  }

  }

  return _fileQueue;

  }

  public void Add(FileInfo file)

  {

  _fileIndexs.Add(file.Name, index);

  _files.Add(file.Name,file);

  FileArea fileAerea = new FileArea(file);

  _fileAeas.Add(file.Name, fileAerea);

  ++index;

  }

  public void Remove(string fileName)

  {

  _fileIndexs.Remove(fileName);

  _files.Remove(fileName);

  _fileAeas.Remove(fileName);

  _fileUploader.Remove(fileName);

  }

  public Dictionary Files

  {

  get

  {

  return _files;

  }

  set

  {

  _files = value;

  }

  }

  public Dictionary FileAreas

  {

  get

  {

  return _fileAeas;

  }

  set

  {

  _fileAeas = value;

  }

  }

  public Dictionary FileUploader

  {

  get

  {

  return _fileUploader;

  }

  set

  {

  _fileUploader = value;

  }

  }

  public int GetFileIndex(string fileName)

  {

  int i=-1;

  if (_fileIndexs.ContainsKey(fileName))

  {

  i = _fileIndexs[fileName];

  }

  return i;

  }

  public void Clear()

  {

  string[] tempFileNames=new string[this.Files.Count];

  this.Files.Keys.CopyTo(tempFileNames,0);

  foreach (string fileName in tempFileNames)

  {

  this.Remove(fileName);

  }

  }

  }

  }

  FileArea用于构建每个文件的UI展示:

  using System;

  using System.Net;

  using System.Windows;

  using System.Windows.Controls;

  using System.Windows.Documents;

  using System.Windows.Ink;

  using System.Windows.Input;

  using System.Windows.Media;

  using System.Windows.Media.Animation;

  using System.Windows.Shapes;

  using System.Windows.Media.Imaging;

  using System.IO;

  using Cmj.MyWeb.MySilverlight.MyUserControl.Button;

  namespace CmjUpload

  {

  public class FileArea

  {

  private FileInfo _file = null;

  //private int _number = 0;

  private Grid _container = null;

  private TextBlock _name = null;

  private Image _thumnail = null;

  private ProgressBar _progress = null;

  private TextBlock _size = null;

  private TextBlock _percent = null;

  private Cancel _cancel = null;

  private Pause _pause = null;

  private Play _continue = null;

  private Check _complete = null;

  public FileArea(FileInfo file)

  {

  _file = file;

  //_number = number;

  _container = new Grid();

  _container.Name = "fileArea_container_" + file.Name;

  _container.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(60)});

  _container.ColumnDefinitions.Add(new ColumnDefinition());

  _container.ColumnDefinitions.Add(new ColumnDefinition() { Width=new GridLength(60)});

  _container.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(60) });

  _container.Height = 50;

  _thumnail = new Image();

  _thumnail.Name = "fileArea_thumnail_" + file.Name;

  _thumnail.Height = 40;

  _thumnail.Source = FileIcon.Instance().GetThumbnailImage(file);

  _thumnail.VerticalAlignment = VerticalAlignment.Bottom;

  _thumnail.HorizontalAlignment = HorizontalAlignment.Center;

  Grid.SetColumn(_thumnail, 0);

  _progress = new ProgressBar();

  _progress.Name = "fileArea_progress_" + file.Name;

  _progress.Minimum = 0;

  _progress.Maximum = 100;

  _progress.Value = 0;

  _progress.Height = 20;

  _progress.VerticalAlignment = VerticalAlignment.Bottom;

  //_progress.HorizontalAlignment = HorizontalAlignment.Center;

  Grid.SetColumn(_progress, 1);

  _name = new TextBlock();

  _name.Name = "fileArea_name_" + file.Name;

  _name.Text = file.Name;

  _name.VerticalAlignment = VerticalAlignment.Bottom;

  _name.HorizontalAlignment = HorizontalAlignment.Left;

  _name.Margin = new Thickness(10, 0, 0, 2);

  Grid.SetColumn(_name, 1);

  _percent = new TextBlock();

  _percent.Name = "fileArea_percent_" + file.Name;

  _percent.VerticalAlignment = VerticalAlignment.Bottom;

  _percent.HorizontalAlignment = HorizontalAlignment.Right;

  _percent.Margin = new Thickness(0, 0, 10, 2);

  Grid.SetColumn(_percent, 1);

  _size = new TextBlock();

  _size.Name = "fileArea_size_" + file.Name;

  _size.VerticalAlignment = VerticalAlignment.Bottom;

  _size.HorizontalAlignment = HorizontalAlignment.Right;

  Grid.SetColumn(_size, 2);

  _cancel = new Cancel();

  _cancel.Name = "fileArea_cancel_"+file.Name;

  _cancel.Width = 15;

  _cancel.Height = 15;

  _cancel.VerticalAlignment = VerticalAlignment.Bottom;

  //_cancel.Click += new RoutedEventHandler(_cancel_Click);

  Grid.SetColumn(_cancel, 3);

  _pause = new Pause();

  _pause.Name = "fileArea_pause_" + file.Name;

  _pause.Width = 15;

  _pause.Height = 15;

  _pause.VerticalAlignment = VerticalAlignment.Bottom;

  _pause.Visibility = Visibility.Collapsed;

  Grid.SetColumn(_pause, 3);

  _continue = new Play();

  _continue.Name = "fileArea_continue_" + file.Name;

  _continue.Width = 15;

  _continue.Height = 15;

  _continue.VerticalAlignment = VerticalAlignment.Bottom;

  _continue.Visibility = Visibility.Collapsed;

  Grid.SetColumn(_continue, 3);

  _complete = new Check();

  _complete.Name = "fileArea_complete_" + file.Name;

  _complete.Width = 18;

  _complete.Height = 18;

  _complete.VerticalAlignment = VerticalAlignment.Bottom;

  _complete.Visibility = Visibility.Collapsed;

  Grid.SetColumn(_complete, 3);

  _container.Children.Add(_thumnail);

  _container.Children.Add(_progress);

  _container.Children.Add(_size);

  _container.Children.Add(_name);

  _container.Children.Add(_percent);

  _container.Children.Add(_cancel);

  _container.Children.Add(_pause);

  _container.Children.Add(_continue);

  _container.Children.Add(_complete);

  }

  public Grid Container

  {

  get

  {

  return _container;

  }

  set

  {

  _container = value;

  }

  }

  public TextBlock Name

  {

  get

  {

  return _name;

  }

  set

  {

  _name = value;

  }

  }

  public Image Thumnail

  {

  get

  {

  return _thumnail;

  }

  set

  {

  _thumnail = value;

  }

  }

  public ProgressBar Progress

  {

  get

  {

  return _progress;

  }

  set

  {

  _progress = value;

  }

  }

  public TextBlock Size

  {

  get

  {

  return _size;

  }

  set

  {

  _size = value;

  }

  }

  public TextBlock Percent

  {

  get

  {

  return _percent;

  }

  set

  {

  _percent = value;

  }

  }

  public Cancel Cancel

  {

  get

  {

  return _cancel;

  }

  set

  {

  _cancel = value;

  }

  }

  public Pause Pause

  {

  get

  {

  return _pause;

  }

  set

  {

  _pause = value;

  }

  }

  public Play Continue

  {

  get

  {

  return _continue;

  }

  set

  {

  _continue = value;

  }

  }

  public Check Complete

  {

  get

  {

  return _complete;

  }

  set

  {

  _complete = value;

  }

  }

  }

  }

  ExposeInterface用于向客户端调用提供操作接口:

  using System;

  using System.Net;

  using System.Windows;

  using System.Windows.Controls;

  using System.Windows.Documents;

  using System.Windows.Ink;

  using System.Windows.Input;

  using System.Windows.Media;

  using System.Windows.Media.Animation;

  using System.Windows.Shapes;

  using System.Collections.Generic;

  using System.Windows.Browser;

  namespace CmjUpload.Config

  {

  public class ExposeInterface

  {

  private static object _lock = new object();

  private static ExposeInterface _exposeInterface = null;

  private string _fileTypes = string.Empty;

  private string _fileDialogFilter = string.Empty;

  private long _limitSize = 0;

  private int _limitCount = 0;

  private ExposeInterface()

  {

  }

  public static ExposeInterface Instance()

  {

  lock (_lock)

  {

  if (_exposeInterface == null)

  {

  _exposeInterface = new ExposeInterface();

  }

  }

  return _exposeInterface;

  }

  [ScriptableMember]

  public string FileTypes //ex:*.jpg|*.gif

  {

  get

  {

  return _fileTypes;

  }

  set

  {

  _fileTypes = value;

  }

  }

  [ScriptableMember]

  public string FileDialogFilter

  {

  get

  {

  if (this._fileDialogFilter == string.Empty&&this._fileTypes!=string.Empty)

  {

  string[] types = this._fileTypes.Split('|');

  string[] filters=new string[types.Length];

  for(int i=0;i

  {

  filters[i] = "("+types[i] +")|"+ types[i];

  }

  _fileDialogFilter = string.Join("|",filters);

  }

  return _fileDialogFilter;

  }

  set

  {

  _fileDialogFilter = value;

  }

  }

  [ScriptableMember]

  public long LimitSize//单位 MB

  {

  get

  {

  return _limitSize;

  }

  set

  {

  _limitSize = value;

  }

  }

  [ScriptableMember]

  public int LimitCount

  {

  get

  {

  return _limitCount;

  }

  set

  {

  _limitCount = value;

  }

  }

  [ScriptableMember]

  public string RequestUrl

  {

  get;

  set;

  }

  public List GetFileExtensions()

  {

  List extensions = new List();

  string[] types = this._fileTypes.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);

  foreach(string type in types)

  {

  extensions.Add(type.TrimStart('*'));

  }

  return extensions;

  }

  }

  }

  CmjUpload用于提供给服务器端进行文件操作,服务端只需要简单调用其Save方法就可以进行文件保存:

  using System;

  using System.Collections.Generic;

  using System.Web;

  using System.IO;

  namespace CmjUpload.Web.Util

  {

  public class CmjUpload

  {

  public static void Save(string relationPath)

  {

  string fileName = HttpContext.Current.Request["fileName"];

  Save(relationPath,fileName);

  }

  public static void Save(string relationPath,string outputName)

  {

  string status = HttpContext.Current.Request["status"];

  if (status == "Start")

  {

  using (FileStream fs = File.Create(Path.Combine(relationPath, outputName)))

  {

  SaveFile(HttpContext.Current.Request.InputStream, fs);

  }

  }

  else if (status == "Uploading")

  {

  using (FileStream fs = File.Open(Path.Combine(relationPath, outputName), FileMode.Append))

  {

  SaveFile(HttpContext.Current.Request.InputStream, fs);

  }

  }

  else if (status == "Completed")

  {

  HttpContext.Current.Response.Write("{success:true}");

  }

  }

  private static 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);

  }

  }

  }

  }

  OK,其他的代码就不再贴出了,看一下客户端如何使用吧。

  为了方便使用客户端提供一个公用js类CmjUpload.js:

  //注意要在控件加载完之后调用,建议放到插件onload事件中初始化(

  var CmjUpload = function (options) {

  var uploader = null;

  if (options.hasOwnProperty("id")) {//组件id

  uploader = document.getElementById(options.id);

  } else {

  alert("Please configure the id attribute before use CmjUpload component!");

  return;

  }

  if (options.hasOwnProperty("requestUrl")) {//请求的url

  uploader.content.cmjUpload.RequestUrl = options.requestUrl;

  } else {

  alert("Please configure the requestUrl attribute before use CmjUpload component!");

  return;

  }

  if (options.hasOwnProperty("fileTypes")) {//文件类型限制

  uploader.content.cmjUpload.FileTypes = options.fileTypes;

  }

  if (options.hasOwnProperty("limitCount")) {//每批次上传的文件数

  uploader.content.cmjUpload.LimitCount = options.limitCount;

  }

  if (options.hasOwnProperty("limitSize")) {//单个文件大小限制

  uploader.content.cmjUpload.LimitSize = options.limitSize;

  }

  }

  CmjUpload.prototype.onBeforeFileUpload = function () {//单个文件上传之前执行

  }

  CmjUpload.prototype.onFileUploading = function () { //单个文件上传时执行

  }

  CmjUpload.prototype.onFileUploaded = function () {//单个文件上传完毕执行

  }

  CmjUpload.prototype.onBatchUploaded = function () {//整个批次的文件上传完毕执行

  }

  然后在页面添加上传组件(本地化语言在param中进行配置):

  

  

  

  

  

  

  

  

  

  

Get Microsoft Silverlight

  

  使用时在页面引用该类,进行id和url等信息配置,具体的配置内容上面js已经注释的很清楚,这里不再赘余。例如对页面做如下配置:

  pluginLoaded=function(){

  var upload = new CmjUpload({ id: 'cmjUpload1', requestUrl: 'http://localhost:3407/Upload.aspx', fileTypes: '*.jpg|*.png|*.wmv|*.rar|*.iso', limitCount: 5, limitSize: 150 });

  }

  后台文件执行文件保存操作:

  using System;

  using System.Collections.Generic;

  using System.Linq;

  using System.Web;

  using System.Web.UI;

  using System.Web.UI.WebControls;

  using System.IO;

  namespace CmjUpload.Web

  {

  public partial class Upload : System.Web.UI.Page

  {

  protected void Page_Load(object sender, EventArgs e)

  {

  CmjUpload.Web.Util.CmjUpload.Save("f:\");

  }

  }

  }

  下面是使用效果:

  类型限制

大小限制

数量限制

删除一个文件

上传中

上传暂停

完成上传

  手动清空(即使不手动清空继续上传文件会自动清空,清空操作主要用于上传暂停后不需要上传的清空)

下面看看本地化设置为英文后的效果

  我们通过修改LimitCount来看一下大文件传输,好几个G的文件同时传输也没有问题

  OK,***附上组件下载,使用方法上面说的也比较清楚了。关于组件源代码就不再提供下载了,相信读完这篇文章要实现并不难,真正需要的话可以给我留言或发邮件KenshinCui@hotmail.com。

  组件下载

  本作品采用知识共享署名 2.5 中国大陆许可协议进行许可,欢迎转载,演绎或用于商业目的。但转载请注明来自崔江涛(KenshinCui),并包含相关链接。

【编辑推荐】

  1. Silverlight***动态和未来前景
  2. 微软正式发布Silverlight 5
  3. 微软能否撑起Silverlight的明天?
  4. 基于Silverlight的网络操作系统SilveOS
  5. Silverlight企业应用开发实践之AgileEAS.NET

网站题目:分析Silverlight文件上传组件
文章路径:http://www.shufengxianlan.com/qtweb/news8/42558.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联