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

炫酷的 ASP.NET MVC 图片上传器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (90投票s)

2012年5月7日

CPOL

16分钟阅读

viewsIcon

516997

downloadIcon

25195

让我们使用 jQuery、AJAX 和 MVC 构建一个类似 Google 的图片上传器示例项目。

A list of all images uploaded with this sample app

引言

将图片上传到网站是几乎所有网站都需要的功能。这个需求可能源于 CMS 背景,也可能源于用户生成的内容,如个人资料、帖子和各种其他用途。一个常见的问题是缺少图片编辑选项。本文档不想构建一个非常丰富的编辑器,但我们希望构建一个不仅能处理普通文件上传的图片上传器。此外,我们的图片上传器还应该能够轻松地进行裁剪。

The Google image uploader

我们的图片上传器将拥有与 Google 在联系人 Web 应用程序中使用的图片上传器(大约)相同的选项。我们将包含以下选项:

  • 从本地文件系统(标准文件上传)
  • 从其他 URL(互联网资源)
  • 从 Flickr(按图片搜索,从结果列表中选择一张图片)

我们将使用 ASP.NET MVC 3 作为服务器端技术。客户端将大量使用 jQuery 和一个名为 ImageAreaSelect 的插件。该插件将使我们能够在客户端浏览器中(即时)进行裁剪。为了预览本地文件系统中的图片,我们将使用 FileAPI,这是当前所有浏览器都支持的功能。

背景

整个过程的基本思路如下所示。

The basic outline for our fancy image uploader

因此,ASP.NET MVC 负责生成页面(这个页面不需要太多生成,因为它大部分是静态的)。然后,我们的 JavaScript 代码将处理生成网站上的客户端事件。这些事件主要影响页面内的 <form>。在这里,我们将提供三种可能性(文件上传、URL、Flickr)。我们需要一个复选框来指示当前选择的方法。表单的一些元素将被隐藏,并通过 jQuery 插件进行更改,该插件帮助我们完成裁剪。

我们不会实现任何回退代码,尽管也可以实现非 JavaScript 解决方案。原因主要有两个:

  1. 在图片实际上传之前进行预处理的整个过程在没有 JavaScript 的情况下是多余的。因此,本文档的很大一部分将是多余的。
  2. 代码将需要包含一些有趣的功能,例如之前隐藏的状态将通过 JavaScript 重新显示,反之亦然。总的来说,任何考虑非 JavaScript 浏览器的页面都必须首先处理这些用户。然后,JavaScript 将为第二种(通常大得多的)用户类型修改页面:启用 JavaScript 的用户。我们希望在这里专注于我们的任务。

准备好了吗?那么,让我们开始构建这个炫酷的图片上传器吧!

实现

我们从 ASP.NET MVC 3 包中的“Internet 应用程序”模板开始。为了简化,我们删除了所有与数据库或用户账户相关的部分。通常,网页会包含类似这样的内容 - 但对于这个任务,这两者都不是必需的。

让我们从将实际从客户端传输到服务器的模型开始

public class UploadImageModel
{
	[Display(Name = "Internet URL")]
	public string Url { get; set; }

	public bool IsUrl { get; set; }

	[Display(Name = "Flickr image")]
	public string Flickr { get; set; }

	public bool IsFlickr { get; set; }

	[Display(Name = "Local file")]
	public HttpPostedFileBase File { get; set; }

	public bool IsFile { get; set; }
	
	[Range(0, int.MaxValue)]
	public int X { get; set; }

	[Range(0, int.MaxValue)]
	public int Y { get; set; }

	[Range(1, int.MaxValue)]
	public int Width { get; set; }

	[Range(1, int.MaxValue)]
	public int Height { get; set; }
}

我们将模型命名为 UploadImageModel。这不是必需的约定,不像 *Controller 是任何控制器的约定。但是,这对于区分控制器-视图交换模型和其他所有类非常有用。

接下来是生成一些操作。我们将以下操作方法放在 HomeController

public ActionResult UploadImage()
{
	return View();
}

这将显示相应的视图。使用当前的(默认)路由选项,该操作的 URL 是“~//Home/UploadImage”。请求类型应使用 **GET** 方法。在处理视图和所有客户端内容之前,我们将需要另一个操作来接收发布的表单数据。让我们先看一下这个操作

[HttpPost]
public ActionResult UploadImage(UploadImageModel model)
{
	if (ModelState.IsValid)
	{
		Bitmap original = null;
		var name = "newimagefile";
		var errorField = string.Empty;

		if (model.IsUrl)
		{
			errorField = "Url";
			name = GetUrlFileName(model.Url);
			original = GetImageFromUrl(model.Url);
		}
		else if (model.IsFlickr)
		{
			errorField = "Flickr";
			name = GetUrlFileName(model.Flickr);
			original = GetImageFromUrl(model.Flickr);
		}
		else // model.IsFile should be checked !
		{
			errorField = "File";
			name = Path.GetFileNameWithoutExtension(model.File.FileName);
			original = Bitmap.FromStream(model.File.InputStream) as Bitmap;
		}

		if (original != null)
		{
			var fn = Server.MapPath("~/Content/img/" + name + ".png");
			var img = CreateImage(original, model.X, model.Y, model.Width, model.Height);
			img.Save(fn, System.Drawing.Imaging.ImageFormat.Png);
			return RedirectToAction("Index");
		}
		else
			ModelState.AddModelError(errorField, "Your upload did not seem valid. Please try again using only correct images!");
	}

	return View(model);
}

乍一看,这似乎很复杂。这个操作到底做了什么?首先,检查模型是否有效。这是一个相当简单的检查,仅涉及我们在模型中设置的数据注释。在检查了简单值之后,我们可以专注于更复杂的内容。我们只需查看每种可能性(URL、Flickr 和从本地文件系统上传),然后执行一些方法来找出文件名和图像本身。一旦我们有了(原始)图像,我们就可以进行进一步的转换。在这种情况下,我们想要裁剪图像。

为了使其正常工作,我们实现了一些辅助方法。这些可以作为扩展方法包含。在这种情况下,我们将适当的方法写在同一个控制器中,以便将所有内容集中在一起。让我们从一个相对容易的方法开始

string GetUrlFileName(string url)
{
	var parts = url.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
	var last = parts[parts.Length - 1];
	return Path.GetFileNameWithoutExtension(last);
}

此方法仅将 URL 分成几部分。最后一部分必须包含图像的文件名。因此,我们可以应用常用的 Path.GetFileNameWithoutExtension() 方法。下一个可能有趣的方法是 GetImageFromUrl() 方法。此方法将通过给定的 URL 从任何 Web 服务器请求一个网页并返回图像。

Bitmap GetImageFromUrl(string url)
{
	var buffer = 1024;
	Bitmap image = null;

	if (!Uri.IsWellFormedUriString(url, UriKind.Absolute))
		return image;

	using (var ms = new MemoryStream())
	{
		var req = WebRequest.Create(url);

		using (var resp = req.GetResponse())
		{
			using (var stream = resp.GetResponseStream())
			{
				var bytes = new byte[buffer];
				var n = 0;

				while ((n = stream.Read(bytes, 0, buffer)) != 0)
					ms.Write(bytes, 0, n);
			}
		}

		image = Bitmap.FromStream(ms) as Bitmap;
	}
	
	return image;
}

我们首先检查传入的 URL 是否是一个真实的绝对 URL。如果是这种情况,我们就可以开始请求。此请求会将响应数据流式传输到内存流中。这是为了获得一个可搜索的流。之后,我们从此内存流创建图像。然后最终返回图像。

在我们的操作代码中可以检测到的最后一个方法是 CreateImage() 方法。此方法基本上会执行裁剪。对于熟悉 GDI+ 的任何人来说,代码都非常直观。

Bitmap CreateImage(Bitmap original, int x, int y, int width, int height)
{
	var img = new Bitmap(width, height);

	using (var g = Graphics.FromImage(img))
	{
		g.SmoothingMode = SmoothingMode.AntiAlias;
		g.InterpolationMode = InterpolationMode.HighQualityBicubic;
		g.DrawImage(original, new Rectangle(0, 0, width, height), x, y, width, height, GraphicsUnit.Pixel);
	}

	return img;
}

为了完成我们代码的服务器端,我们需要附加相应的视图。在我们的例子中,我们只有一个视图需要附加:UploadImage.cshtml(是的,我们将在此文档中使用 Razer 作为视图引擎)。为了最大限度地提高效率,我们将使用 UploadImageModel 作为模型,并使用 **Create** 作为脚手架选项来搭建第一个草稿。生成的 HTML 距离我们的最终目标还很远。为什么我们仍然应该使用脚手架选项?嗯,首先,它是一个很好的测试点。当然,单元测试我们的控制器是更好的测试,但有时你只需要一个“它已经奏效了吗?!”的即时测试。如果是这种情况,那么我们刚刚在几秒钟内提供了一个测试环境。使用脚手架选项的第二个原因是,我们已经有了一个良好的基础。从这一点开始,我们所要做的就是删除、移动和可能添加一些东西。

Scaffolding the UploadImage view

那么需要进行哪些类型的更改呢?首先,我们将大量依赖 JavaScript。因此,我们将不得不添加

  • jQuery 的 ImageAreaControl 插件,以及
  • 一个包含我们的自定义脚本的脚本标签,用于 UploadImage 站点。

由于我们应该始终将所有脚本放在页面底部,因此有必要在此处使用区域(当然,也可以使用 ViewBag 和扩展方法,或其他方法)。因此,我们将不得不包含以下代码片段以访问脚本

@section Scripts
{
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")"></script>
<script src="@Url.Content("~/Scripts/jquery.imgareaselect.js")"></script>
<script>
	/* this is to come */
</script>
}

为此,我们需要包含文件 jquery.imgareaselect.js。可以从 http://odyniec.net/projects/imgareaselect/ 下载此文件。我们应该将其放在 Scripts 文件夹中。为了使此代码(包括 section Scripts 指令)正常工作,我们已修改主布局页面,使其如下所示:

<!-- Everything of the page ... -->
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")"></script>
<script src="@Url.Content("~/Scripts/modernizr-1.7.min.js")"></script>
@RenderSection("Scripts", false)
</body>
</html>

A short look at the scaffolded page

现在,如果我们快速查看 UploadImage.cshtml 的脚手架页面,我们会注意到缺少两件重要的事情:

  1. 首先,为了进行任何 File 上传,我们需要确保 <form> 标签具有正确的 enctype 属性。现在我们只有 Html.BeginForm()
  2. 其次,我们当前缺少一个包含 Model.File 的行。这是脚手架的局限性。

这两者都可以轻松解决。对于第一个问题,我们将仅为 BeginForm() 方法指定一些参数。最后,它将如下所示:

Html.BeginForm("UploadImage", "Home", FormMethod.Post, new { enctype = "multipart/form-data" })

对于第二个问题,我们有多种选择。我们可以编写纯 HTML,也可以为 Model.File 属性的类型编写一个扩展方法。后者当然是推荐的。通过向源添加一个名为 HtmlExtensions 的静态类,我们可以插入一些扩展方法,例如:

public static class HtmlExtensions
{
	public static MvcHtmlString File(this HtmlHelper html, string name)
	{
		var tb = new TagBuilder("input");
		tb.Attributes.Add("type", "file");
		tb.Attributes.Add("name", name);
		tb.GenerateId(name);
		return MvcHtmlString.Create(tb.ToString(TagRenderMode.SelfClosing));
	}

	public static MvcHtmlString FileFor(this HtmlHelper html, Expression> expression)
	{
		string name = GetFullPropertyName(expression);
		return html.File(name);
	}

	#region Helpers

	static string GetFullPropertyName(Expression> exp)
	{
		MemberExpression memberExp;

		if (!TryFindMemberExpression(exp.Body, out memberExp))
			return string.Empty;

		var memberNames = new Stack();

		do
		{
			memberNames.Push(memberExp.Member.Name);
		}
		while (TryFindMemberExpression(memberExp.Expression, out memberExp));

		return string.Join(".", memberNames.ToArray());
	}

	static bool TryFindMemberExpression(Expression exp, out MemberExpression memberExp)
	{
		memberExp = exp as MemberExpression;

		if (memberExp != null)
			return true;

		if (IsConversion(exp) && exp is UnaryExpression)
		{
			memberExp = ((UnaryExpression)exp).Operand as MemberExpression;

			if (memberExp != null)
				return true;
		}

		return false;
	}

	static bool IsConversion(Expression exp)
	{
		return (exp.NodeType == ExpressionType.Convert || exp.NodeType == ExpressionType.ConvertChecked);
	}

	#endregion
}

这为我们提供了两个 HtmlHelper 辅助实例的扩展方法,在任何包含使用命名空间和程序集的视图中。为了与 ASP.NET MVC 团队提供的现有 HtmlHelper 扩展方法保持一致,我们创建了 File()FileFor() 方法。第一个是一个简单的 HTML 辅助方法,第二个可以与强类型模型一起使用,以避免输入错误。

在将包含这两个扩展方法的静态类的命名空间添加到 Views 文件夹的 Web.config 文件后,我们就可以在视图中插入以下行:

<div class="editor-label">
	@Html.LabelFor(model => model.File)
</div>
<div class="editor-field">
	@Html.FileFor(model => model.File)
	@Html.ValidationMessageFor(model => model.File)
</div>

现在,真正的转换即将到来。我们希望实现以下目标:

  • 可能的选择应放置在页面的左侧。
  • 应在左侧选项的右侧显示的图片预览中进行图片裁剪。
  • 上传按钮应放置在页面底部。

因此,<form> 标签内的新的标记是:

@Html.HiddenFor(model => model.X)
@Html.HiddenFor(model => model.Y)
@Html.HiddenFor(model => model.Width)
@Html.HiddenFor(model => model.Height)
<div id="upload-choices">
	<div class="editor-row">
		<div class="editor-label">
			@Html.EditorFor(model => model.IsUrl)
			@Html.LabelFor(model => model.Url)
		</div><div class="editor-field">
			@Html.EditorFor(model => model.Url)
			@Html.ValidationMessageFor(model => model.Url)
		</div>
	</div>
	<div class="editor-row">
		<div class="editor-label">
			@Html.EditorFor(model => model.IsFlickr)
			@Html.LabelFor(model => model.Flickr)
		</div><div class="editor-field">
			@Html.EditorFor(model => model.Flickr)
			@Html.ValidationMessageFor(model => model.Flickr)
		</div>
	</div>
	<div class="editor-row">
		<div class="editor-label">
			@Html.EditorFor(model => model.IsFile)
			@Html.LabelFor(model => model.File)
		</div><div class="editor-field">
			@Html.FileFor(model => model.File)
			@Html.ValidationMessageFor(model => model.File)
		</div>
	</div>
	<div class="editor-row">
		@Html.ValidationSummary(true)
	</div>
</div>
<div id="upload-cut">
	<img alt="Field for image cutting" id="preview" src="@Url.Content("~/Content/empty.png")" />
</div>
<div class="clear">
	<button type="submit">Upload</button>
</div>

基本上,我们只是将这四个自由度(X、Y、宽度和高度)修改为隐藏变量。这些将由我们的 JavaScript 修改。我们清楚地将上传选项与图片裁剪分开了。并且我们添加了容器来形成行。总而言之,这只是标记,如果没有正确的样式,几乎没有用。我们需要在 Site.css 文件中放入以下内容:

#upload-choices {
    width: 450px;
    float: left;
}

#upload-cut {
    margin-left: 480px;
    padding-top: 10px;
    min-width: 150px;
}

#preview {
    max-width: 100%;
    display: block;
}

.editor-row {
    margin: 10px 0;
    height: 40px;
    width: 100%;
}

.editor-row div {
	margin: 0; height: 40px; display: inline-block;
}

.editor-row div.editor-label {
    width: 150px;
}

.editor-row div.editor-field {
    width: 300px;
}

.editor-row .editor-field input {
    width: 300px; height: 100%; box-sizing: border-box;
}

button {
    background: #3F9D4A; border: none; font-size: 1.2em; color: #FFF; padding: 7px 10px;
    border-radius: 4px; font-weight: bold; text-shadow: 0 1px 0 rgba(0,0,0,0.4); margin: 5px 0;
}

button:hover {
    box-shadow: 0 0 10px #666;
}

The UploadImage view after our modifications and with applied styling

既然我们已经从设计角度设置好了一切,我们就需要编写一些炫酷的 JavaScript。这让我们回到了之前介绍的 <script></script> 标签。为了让 jQuery 插件(ImageAreaSelect)正常工作,我们将不得不包含插件(脚本)本身和必需的样式表。两者都可以通过使用区域(如前所述)来插入。我们客户端 JavaScript 的基本程序流程将如下所示:

  1. 用户可以选择任何选项来上传图片。
  2. 一旦选择了某个选项(更改文本/文件上传),复选框就会被启用并立即选中。
  3. 如果启用了多个选项,用户可以通过勾选相应的复选框来选择首选上传方式。
  4. 将通过 **FileAPI** 检查并读取文件,以便显示预览图像。
  5. 提交按钮应在开始时(未选择任何内容)和结束时(发布上传)禁用。

那么,让我们从代码的基本结构开始。这个第一个版本包含了上面列表中的几乎所有内容:

$(document).ready(function () {
    //Get the checkboxes and disable them
    var boxes = $('input[type=checkbox]').attr('disabled', true);

    //Get the preview image and set the onload event handler
    var preview = $('#preview').load(function () {
        setPreview();
        ias.setOptions({
            x1: 0,
            y1: 0,
            x2: $(this).width(),
            y2: $(this).height(),
            show: true
        });
    });

    //Set the 4 coordinates for the cropping
    var setPreview = function (x, y, w, h) {
        $('#X').val(x || 0);
        $('#Y').val(y || 0);
        $('#Width').val(w || preview[0].naturalWidth);
        $('#Height').val(h || preview[0].naturalHeight);
    };

    //Initialize the image area select plugin
    var ias = preview.imgAreaSelect({
        handles: true,
        instance: true,
        parent: 'body',
		onSelectEnd: function (s, e) {
			var scale = preview[0].naturalWidth / preview.width();
			var _f = Math.floor;
			setPreview(_f(scale * e.x1), _f(scale * e.y1), _f(scale * e.width), _f(scale * e.height));
		}
    });

    //Check one of the checkboxes
    var setBox = function (filter) {
        boxes.attr('checked', false)
            .filter(filter).attr({ 'checked': true, 'disabled': false });
    };

    //Initial state of X, Y, Width and Height is 0 0 1 1
    setPreview(0, 0, 1, 1);

    //Flickr

    //What happens if the URL changes?
    $('#Url').change(function () {
        setBox('#IsUrl');
        preview.attr('src', this.value);
    });

    //What happens if the File changes?
    $('#File').change(function (evt) {
        var f = evt.target.files[0];
        var reader = new FileReader();

        if (!f.type.match('image.*')) {
            alert("The selected file does not appear to be an image.");
            return;
        }

        setBox('#IsFile');
        reader.onload = function (e) { preview.attr('src', e.target.result); };
        reader.readAsDataURL(f);
    });

    //What happens if any checkbox is checked ?!
    boxes.change(function () {
        setBox(this);
        $('#' + this.id.substr(2)).change();
    });
	
	//Form button enable / disable
});

两件重要的事情缺失的是 Flickr 上传以及按钮的启用和禁用。后者实际上并不难实现。我们只需要在 setBox() 方法中放置一个类似 $('button').attr('disabled', false) 的方法调用,并将按钮的初始状态设置为禁用。我们还需要为 $('form').submit() 事件分配一个有效的处理程序。一种可能的方式是以下代码:

$('form').submit(function () {
	$('button').attr('disabled', true).text('Please wait ...');
});

不是那么简单的问题是引入 Flickr 图片的图片上传。目前一切都已设置好,我们将 Flickr 字符串视为 URL - 就像 URL 字段一样。因此,这几乎是无用的!所以,我们现在将引入一些非常酷的更改,以便通过 jQuery 和 AJAX 利用 Flickr JSON API。

首先,我们将 Model.Flickr 字段设为隐藏变量,并在该位置放置一个 ID 为 FlickrQuery 的输入字段。现在 JavaScript 函数的任务是获取 FlickrQuery 输入并使用此搜索字符串查询 Flickr。让我们先看看我们的 JavaScript 代码:

//Fetch Flickr images
var fetchImages = function (query) {
	$.getJSON('http://www.flickr.com/services/feeds/photos_public.gne?jsoncallback=?', {
		tags: query,
		tagmode: "any",
		format: "json"
	}, function (data) {
		var screen = $('<div />').addClass('waitScreen').click(function () {
			$(this).remove();
		}).appendTo('body');
		var box = $('<div />').addClass('flickrImages').appendTo(screen);
		$.each(data.items, function (i, v) {
			console.log(data.items[i]);
			$('<img />').addClass('flickrImage').attr('src', data.items[i].media.m).click(function () {
				$('#Flickr').val(this.src).change();
				screen.remove(); //Close it
			}).appendTo(box);
		});
	});
};

//Flickr
$('#FlickrQuery').change(function () {
	fetchImages(this.value);
});

//Just to stay coherent with the other two options
$('#Flickr').change(function () {
	setBox('#IsFlickr');
	preview.attr('src', this.value);
});

我们在这里添加了一个方法,使用公共 JSON API 从 Flickr 获取图片。此选择仅用于演示目的,因为它具有简单且不需要 API 密钥的优点。这种方法的最大缺点是,我们只能从 Flickr 获取一张小图片,并且无法访问(至少我们不知道 URL)原始图片。在服务 URL 和数据映射之后,我们设置了一个回调。此回调会创建一个模态 <div>,其中包含所有返回的图片。

After entering a Flickr search query (tags) it is necessary to select the desired image

模态框使用以下 CSS 指令进行样式设置:

.waitScreen {
    position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.2);
    z-index: 1000000; /* We probably need to be above the ImageAreaSelect */
}

.flickrImages {
    position: absolute; left: 50%; top: 10%; width: 560px; margin-left: -300px; padding: 20px;
    background: #FFF; border-radius: 10px; border: 0; box-shadow: 0 3px 10px #666;
}

.flickrImage {
    height: 96px; margin: 4px; float: left; cursor: pointer;
}

基本上就这么多了。此解决方案未涵盖的是在图片上传过程中调整浏览器窗口大小。这可以通过重新缩放当前保存的坐标(相对于图像的原始尺寸)到屏幕来完成。

使用代码

请随时使用 HomeController 中的必要方法。除 Index() 操作外的所有操作都与图片上传器相关。您可以将它们添加到任何控制器中,因为除了 UploadImageModel 之外,没有其他依赖项。视图也与 UploadImageModel 和用于 File <input> 标签的 HTML 扩展方法相关。请注意 jQuery ImageAreaSelect 插件所需的依赖项(样式表、图像和脚本源文件本身)。

该控制器目前将图像上传到名为“~/Content/img/”的目录中。这仅用于演示目的。通常,我们可能希望将图像放在另一个应用程序、数据库或目录中。因此,这两行需要相应地更改:

var fn = Server.MapPath("~/Content/img/" + name + ".png");
img.Save(fn, System.Drawing.Imaging.ImageFormat.Png);

由于提供了名称和图像本身,找到合适的位置和方法应该没有问题。

为旧版浏览器提供基本的回退

FileAPI 的引入是为了让 JavaScript 开发者能够访问本地文件系统。然而,浏览器支持仍然有限。目前,所有 Internet Explorer 直到当前版本 9 都不支持此功能(Source Can I Use)。我们现在将在 JavaScript 和控制器中包含一个回退。

JavaScript 代码必须检测回退模式并做出适当的反应。

$('#File').change(function (evt) {
    if (evt.target.files === undefined)
        return filePreview();
//Rest remains unchanged
}

现在,我们必须编写 filePreview() 方法。此方法将执行以下操作:

  • 将选定的图像上传到适当的操作。
  • 从 iframe 的响应中检索图像。
  • 在预览(裁剪)区域显示图像。

所需的(JavaScript)代码如下:

var filePreview = function () {
	window.callback = function () { };
	$('body').append('<iframe id="preview-iframe" onload="callback();" name="preview-iframe" style="display:none" />
');
	var action = $('form').attr('target', 'preview-iframe').attr('action');
	$('form').attr('action', '/Home/PreviewImage');
	window.callback = function () {
		setBox('#IsFile');
		var result = $('#preview-iframe').contents().find('img').attr('src');
		preview.attr('src', result);
		$('#preview-iframe').remove();
	};
	$('form').submit().attr('action', action).attr('target', '');
};

总的来说,我们只是遵循上述步骤。我们正在创建 iframe,更改表单,使用它,最后删除所有步骤。然后,回调最终用于显示图像,就像之前显示的那样。我们实际上让 src 属性负责图像,也就是说,我们正在接收一个 base64 字符串。

这是 HomeController 中的代码:

[HttpPost]
public ActionResult PreviewImage()
{
	var bytes = new byte[0];
	ViewBag.Mime = "image/png";

	if (Request.Files.Count == 1)
	{
		bytes = new byte[Request.Files[0].ContentLength];
		Request.Files[0].InputStream.Read(bytes, 0, bytes.Length);
		ViewBag.Mime = Request.Files[0].ContentType;
	}

	ViewBag.Message = Convert.ToBase64String(bytes, Base64FormattingOptions.InsertLineBreaks);
	return PartialView();
}

这里没有新东西!我们(真的应该)只接收一个文件并将其作为 base64 字符串返回。我们是如何返回的?使用局部视图!如果你想知道那个局部视图里有什么:

<img src="data:@ViewBag.Mime;base64,@ViewBag.Message" />

这是整个解决方法。我在 IE9 中测试了此解决方法 - 在那里运行完美。如果您知道旧版 IE 是否也能处理此问题,请告知我。

在模态对话框中使用上传器

另一个必备功能是能够将炫酷的图片上传器显示在模态对话框中。为此,我们将首先设置一些 CSS:

.modal_block
{
    background: rgba(0, 0, 0, 0.3);
    position: fixed;
    width: 100%;
    height: 100%;
    top: 0;
    left: 0;
}

.modal_part
{
    z-index: 100;
    display: none;
}

.modal_dialog
{
    width: 60%;
    height: 60%;
    position: absolute;
    top: 15%;
    left: 15%;
    padding: 5%;
    border-radius: 10px;
    border: 0;
    background: #fff;
    box-shadow: 0 0 5px #000;
}

.modal_dialog:empty
{
    background: #fff url(ajax.gif) no-repeat center center;
}

在这里,我们只是声明了一些类,它们最终将构成模态对话框系统的基石。接下来,我们将图片上传器的内联 JavaScript($(document).ready() 方法中的所有内容)外包到一个名为 **jquery.fancyupload.js** 的新文件中。我们还将稍微重写 JavaScript。最后,新文件看起来像下面的代码:

var initSelect = function (context) {
    context = context || $(document);
    //Get the button of the form of the context
    var button = $('button', context).attr('disabled', true);
    //Get the checkboxes and disable them
    var boxes = $('input[type=checkbox]', context).attr('disabled', true);
    //Get the form of the context
    var form = $('form', context);

    //Get the preview image and set the onload event handler
    var preview = $('#preview', context).load(function () {
        setPreview();
        ias.setOptions({
            x1: 0,
            y1: 0,
            x2: $(this).width(),
            y2: $(this).height(),
            show: true
        });
    });

    //Set the 4 coordinates for the cropping
    var setPreview = function (x, y, w, h) {
        $('#X', context).val(x || 0);
        $('#Y', context).val(y || 0);
        $('#Width', context).val(w || preview[0].naturalWidth);
        $('#Height', context).val(h || preview[0].naturalHeight);
    };

    //Initialize the image area select plugin
    var ias = preview.imgAreaSelect({
        handles: true,
        instance: true,
        parent: 'body',
        onSelectEnd: function (s, e) {
            var scale = preview[0].naturalWidth / preview.width();
            var _f = Math.floor;
            setPreview(_f(scale * e.x1), _f(scale * e.y1), _f(scale * e.width), _f(scale * e.height));
        }
    });

    //Check one of the checkboxes
    var setBox = function (filter) {
        button.attr('disabled', false);
        boxes.attr('checked', false)
            .filter(filter).attr({ 'checked': true, 'disabled': false });
    };

    //Fallback for Browsers with no FileAPI
    var filePreview = function () {
        window.callback = function () { };
        $('body').append('<iframe id="preview-iframe" onload="callback();" name="preview-iframe" style="display:none"></iframe>');
        var action = $('form', context).attr('target', 'preview-iframe').attr('action');
        form.attr('action', '/Home/PreviewImage');
        window.callback = function () {
            setBox('#IsFile');
            var result = $('#preview-iframe').contents().find('img').attr('src');
            preview.attr('src', result);
            $('#preview-iframe').remove();
        };
        form.submit().attr('action', action).attr('target', '');
    };

    //Initial state of X, Y, Width and Height is 0 0 1 1
    setPreview(0, 0, 1, 1);

    //Fetch Flickr images
    var fetchImages = function (query) {
        $.getJSON('http://www.flickr.com/services/feeds/photos_public.gne?jsoncallback=?', {
            tags: query,
            tagmode: "any",
            format: "json"
        }, function (data) {
            var screen = $('<div />').addClass('waitScreen').click(function () {
                $(this).remove();
            }).appendTo('body');
            var box = $('<div />').addClass('flickrImages').appendTo(screen);
            $.each(data.items, function (i, v) {
                console.log(data.items[i]);
                $('<img />').addClass('flickrImage').attr('src', data.items[i].media.m).click(function () {
                    $('#Flickr', context).val(this.src).change();
                    screen.remove();
                }).appendTo(box);
            });
        });
    };

    //Flickr
    $('#FlickrQuery', context).change(function () {
        fetchImages(this.value);
    });

    $('#Flickr', context).change(function () {
        setBox('#IsFlickr');
        preview.attr('src', this.value);
    });

    //What happens if the URL changes?
    $('#Url', context).change(function () {
        setBox('#IsUrl');
        preview.attr('src', this.value);
    });

    //What happens if the File changes?
    $('#File', context).change(function (evt) {
        if (evt.target.files === undefined)
            return filePreview();

        var f = evt.target.files[0];
        var reader = new FileReader();

        if (!f.type.match('image.*')) {
            alert("The selected file does not appear to be an image.");
            return;
        }

        setBox('#IsFile');
        reader.onload = function (e) { preview.attr('src', e.target.result); };
        reader.readAsDataURL(f);
    });

    //What happens if any checkbox is checked ?!
    boxes.change(function () {
        setBox(this);
        $('#' + this.id.substr(2), context).change();
    });

    form.submit(function () {
        button.attr('disabled', true).text('Please wait ...');
    });
};

因此,我们旧的 UploadImage 视图将失去内联 JavaScript,并用以下指令替换它:

<script src="@Url.Content("~/Scripts/jquery.fancyupload.js")"></script>
<script>
    $(document).ready(function () {
        initSelect();
    });
</script>

现在,我们在现有控制器中添加一个新操作:

public HomeController : Controller
{
	/* ... */

	//
	// GET: /Home/UploadImageModal

	public ActionResult UploadImageModal()
	{
		return View();
	}
}

当然,我们需要一个新的视图!为了我们的演示目的,有一个小的 UploadImageModal.cshtml 视图就足够了,其源代码如下:

@{
    ViewBag.Title = "Modal image uploader";
}
@section Styles
{
<link href="@Url.Content("~/Content/Modal.css")" rel="stylesheet" />
<link href="@Url.Content("~/Content/ImageArea.css")" rel="stylesheet" />
}
@section Scripts
{
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")"></script>
<script src="@Url.Content("~/Scripts/jquery.imgareaselect.js")"></script>
<script src="@Url.Content("~/Scripts/jquery.fancyupload.js")"></script>
<script>
    $(document).ready(function () {
        $('.modal_block').click(function (e) {
            $('#tn_select').empty();
            $('.modal_part').hide();
        });
        $('#modal_link').click(function (e) {
            $('.modal_part').show();
            var context = $('#tn_select').load('/Home/UploadImage', function () {
                initSelect(context);
            });
            e.preventDefault();
            return false;
        });
    });
</script>
}
<div class="modal_block modal_part"></div>
<div class="modal_dialog modal_part" id="tn_select"></div>
<h2>Upload an image by using a modal dialog</h2>
<p>
    This is a sample page with basically no content (but there could be one!). Now we will just
    upload content as on the @Html.ActionLink("Upload Image page", "UploadImage"). The only
    difference lies in the usage of a modal dialog to upload the actual image.
</p>
<p>
    <a href="#" id="modal_link">Click here to open modal dialog.</a>
</p>

我们在这里做的不多 - 我们只是包含 CSS 和脚本,并设置内容。真正的图片上传器仍然缺失,但我们准备了一个对话框的容器。内联 JavaScript 有两项职责:

  1. 设置模态对话框,即打开和关闭对话框。打开时,内容应通过 AJAX 加载。
  2. 加载内容后,通过执行我们之前设置的方法来生成功能。因此,我们传递了指定的上下文。

如果我们现在执行代码,我们可能会看到一团糟。这是由于服务器对操作链接的响应。在这里,我们需要区分 Ajax 和非 Ajax 请求。由于 jQuery 已经在附加一个标头变量,并且 ASP.NET MVC 团队包含了一个非常有用的扩展方法,我们只需要使用这些东西来响应局部视图:

public HomeController : Controller
{
	/* ... */

	//
	// GET: /Home/UploadImage

	public ActionResult UploadImage()
	{
		//Just to distinguish between ajax request (for: modal dialog) and normal request
		if (Request.IsAjaxRequest())
		{
			return PartialView();
		}

		return View();
	}
}    

现在一切都设置正确,模态对话框按预期工作。

关注点

这是我为我的新网页编程的一个小功能。我的版本功能更强大一些,因为它允许创建具有固定比例的多个裁剪。这是我的版本的一个截图(我也不使用 Flickr 上传,而是使用现有图片):

The image uploader in usage at my new personal homepage

为了实现固定比例,我只是将 aspectRatio 属性添加到传递给 imgAreaSelect() 构造函数的选项对象中。此属性基本上是一个格式为 w:h 的字符串,其中 w 是宽度,h 是高度。所以 16:9 是宽度与高度比例的一个例子。

然后,我将不同裁剪模式以及不同比例的坐标存储在数组中,这些数组在模型中作为 IEnumerable<int> 等放置。使用 JavaScript,可以将第 i 个 X 坐标替换为 i 是裁剪模式的数量(零基索引)。这效果相当不错,并且可以毫无问题地通过 MVC 传递。因此,模型构建产生了最终的较少问题。

然后视图填充如下:

    for (var i = 0; i < sizes.Length; i++)
    {
    <text>
    @Html.Hidden("X[" + i + "]", 0)
    @Html.Hidden("Y[" + i + "]", 0)
    @Html.Hidden("Width[" + i + "]", 0)
    @Html.Hidden("Height[" + i + "]", 0)
    @Html.Hidden("TargetWidth[" + i + "]", sizes[i].Width)
    @Html.Hidden("TargetHeight[" + i + "]", sizes[i].Height)
    </text>
    }

所以我正在为每次裁剪构建一个 X、Y、宽度和高度的元组。TargetWidth 和 TargetHeight 需要存储,以便 JavaScript 可以读取它们并显示当前活动且可以激活的裁剪模式。比例计算也是通过这两个值完成的。

仅仅展示一点结果 JavaScript:

$('#choice').change(function () {
	choice = this.value * 1;
	var x = $('#X_' + choice + '_').val() * 1;
	var y = $('#Y_' + choice + '_').val() * 1;
	ias.setOptions({
		x1: x,
		y1: y,
		x2: $('#Width_' + choice + '_').val() * 1 + x,
		y2: $('#Height_' + choice + '_').val() * 1 + y,
		aspectRatio: getAspectRatio(choice),
		show: true
	});
});

上面的代码负责显示当前选择的正确图片裁剪视图。getAspectRatio() 方法会评估当前选择并返回正确的纵横比字符串。

历史

  • v1.0.0 | 初始发布 | 2012 年 5 月 6 日。
  • v1.0.1 | 修正了一些拼写错误 | 2012 年 5 月 7 日。
  • v1.0.2 | 修正了更多拼写错误,添加了一些信息 | 2012 年 5 月 8 日。
  • v1.0.3 | 修正了 CSS 代码显示错误 | 2012 年 5 月 9 日。
  • v1.1.0 | 包含了一些基本的回退 | 2012 年 5 月 10 日。
  • v1.2.0 | 包含模态对话框的说明 | 2012 年 7 月 9 日。
© . All rights reserved.