今天说说一个让我,这个很久没有接触Web开发的人混乱了很久的问题,就是什么时候应该使用Http.Get、Http.Post和返回视图的时候什么时候用return View,什么时候要return Redirect。首先声明一下,本人对于Web编程并不精通,下面的一些解释和看法可能有错误,希望各位看到了的话回复一下,纠正本人的错误同时也别误导了其他人。

好了先说一下这个Http.Get和Http.Post。在一个Controller Action方法上面我们可以通过Attribute声明这个Action的一些属性,比如Authorize属性表示这个方法需要登录(权限)验证,ActionName属性可以重定义这个Action的名字(默认是方法名)。还有一个Attribute标识了这个Action可以接受哪种Http Verb的请求,包括Http.Get、Http.Post、Http.Put、Http.Delete和Http.Head。其中最经常使用到的就是Http.Get和Http.Post两个值。

Http.Get一般来说(基本可以把这个定语去掉)是在请求数据的时候使用的,也就是说这个Action操作只会读取数据并显示到页面上,而不会对数据进行任何修改。他的最大特点就是,用户可以通过指向这个Action的URL就可以直接访问到数据,有点RESTful的味道。对应到我们的ASP.NET MVC 1(以下简称MVC)的项目中,Http.Get用于显示内容的Action上面。比如网站的首页一般都是只需要读取数据并显示的,所以Home/Index的Action都会是Http.Get的。

Http.Post则不同,它是通过页面的Form(表单)Post之后所调用的Action。(注:当然可以通过Form的Method参数指定使用Get或者Post Verb提交。)一般来说,Post之后的页面将会对后台数据进行修改,不论是添加、删除或是修改。相对于Get,Post方法是无法直接通过浏览器的URL进行访问的,只有通过特定的网页进行提交才能执行,所以相对于Get,Post的安全性也稍微高些。(注:仍旧可以通过Mock表单来进行伪造的提交,但是MVC为我们提供了一个防范措施。)对应到MVC的项目,对于数据的修改操作则都会通过Post Action进行操作。

在新建一个MVC项目之后,AccountController中就有相似的操作。

        public ActionResult Register()
        {

            ViewData["PasswordLength"] = MembershipService.MinPasswordLength;

            return View();
        }

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Register(string userName, string email, string password, string confirmPassword)
        {

            ViewData["PasswordLength"] = MembershipService.MinPasswordLength;

            if (ValidateRegistration(userName, email, password, confirmPassword))
            {
                // Attempt to register the user
                MembershipCreateStatus createStatus = MembershipService.CreateUser(userName, password, email);

                if (createStatus == MembershipCreateStatus.Success)
                {
                    FormsAuth.SignIn(userName, false /* createPersistentCookie */);
                    return RedirectToAction("Index", "Home");
                }
                else
                {
                    ModelState.AddModelError("_FORM", ErrorCodeToString(createStatus));
                }
            }

            // If we got this far, something failed, redisplay form
            return View();
        }

上面的代码显示了“注册”逻辑。第一个Action没有显式声明AcceptVerb则表示它接受Http.Get的请求,只是简单的显示一个页面。而第二个Action则声明了Http.Post属性,它会在用户点击了注册页面的Submit按钮提交表单后运行,检查数据正确后将新的用户插入数据库(如果后台使用数据库保存数据的话)然后返回首页(Homt/Index)。

所以对于Http.Get和Http.Post,可以简单的认为,如果这个Action有修改数据的操作则要使用Http.Post,否则就可以使用Http.Get。

在第二个Register函数中,可以看到使用了两种不用的方法来返回视图(View)。一个是return RedirectToAction,注册成功之后返回首页的。另一个是末尾的return View,在注册失败(可能是检查未通过,也可能是添加数据的时候出错)的时候返回当前的注册页面并显示错误信息的。那么问题是,这两种返回试图的方法究竟应该在什么时候调用呢?

对于return View,MVC框架不会发起一个新的Http请求(这一点有待考证),所以在Http.Get Action中使用。而且,在Post方法中当数据修改失败需要返回当前页面并显示错误信息的时候也会使用,这样视图表单中数据的数据将会保留,而且View中的ModelState信息等也都会保留。而对于return Redirect,无论是直接Redirect方法还是RedirectToAction方法,都会重新发起一个Http请求并且清楚所有的表单信息和ModelState信息,一般都是在Post方法中修改数据成功后返回列表页面(或者结果页面)的时候使用。比如上面这个Action,注册成功之后将会通过一个新的Http请求执行Home Contrller的Index Action来显示首页。

而且由于return View方法没有发起新的Http请求,如果在Post Action中返回了别的视图,则有可能造成URL和View不匹配的情况。比如我们将上面的return RedirectToAction方法修改为return View("LogOn")来直接返回登录页面,则会看到如下的情形。URL显示的是Account/Register但是现实的页面却是LogOn,这一点和MVC中“一个URL对应唯一的页面”也是不相符合的。在这种情况下,如果用户复制了URL并且发送给别的人,那么他在打开这个URL的时候所看到的页面并不是你现在所看到的。这也损失了MVC的一大优点,就是可以通过URL将你所见到的页面分发给别人。Capture1

而且,如果在这种情形下按了浏览器的刷新按钮(F5),你还会发现浏览器给你提示了一个消息。首先说明一下当按了刷新按钮之后发生了什么。由于我们在Post Action中直接使用return View返回试图,浏览器不会认为这是一个新的Http请求,所提交的表单信息都存在于Http请求里面,所以当刷新本页面的时候,浏览器将会把表单信息再次提交给服务器,实质上是出现了重复提交。而这个消息框是浏览器发现相同的表单被重复请求了两次而特别加上的确认信息,用来防止重复提交的。这个技术不是TCP/IP协议或者HTTP提供的,而是现代浏览器提供的。这种现象也是我们在做MVC的时候应该避免的,而根源就在于使用了错误视图返回方式。正确的做法是通过return Redirect重定向到结果页面,或者显示结果的Action上,然后交由这个页面或Action进行视图展示。这种方法,一些人称之为Web开发的PRG模式(Post – Redirect - Get)。

image

总结一下,Http.Get Verb用于只读取数据并显示的Action上;Http.Post Verb则用于需要对数据库进行修改的Action上。return View方法不会发起新的Http请求,所以只是用户Get Action中用来返回结果,或者Post Action中返回当前页面并显示错误信息。而return Redirect方法将会重定向到一个URL或者Action上面并且会发起新的Http请求,所以只会用于Post Action中数据修改完毕返回结果页面时。

在实际开发中,可能会遇到Get Action显示的页面有一些逻辑,需要从数据库取,然后通过ViewModel传递给视图显示的情况。不像上面例子里面那么简单的LogOn Action。在对应Post方法失败返回当前页的时候,简单使用return View就不行了,因为这个View是需要一些ViewModel支持的。一个方法是在Post Action里面,return View之前进行和Get方法一样的数据读取操作,然后通过return View(model)来显示视图。或者,可以直接调用Get Action这个函数(注意是调用函数而不是调用Action),这样执行Action函数的数据读取操作然后return View(model)。

        [AcceptVerbs(HttpVerbs.Get)]
        public ActionResult MyAction()
        {
            object viewModel = new object();
            return View(viewModel);
        }

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult MyAction(object args)
        {
            var validationResult = false;
            // some validation code
            // ...
            if (validationResult)
            {
                // update the data
                return RedirectToAction("Index");
            }
            else
            {
                return MyAction();
            }
        }
posted @ 2009-09-04 11:46 妖居 阅读(1006) 评论(6) 编辑

自从微软推出了ASP.NET MVC 1.0(此后简称MVC)这个新的网站框架之后,出现了一大批解读MVC的文章。拜读了老赵AnyTao的一些文章,受益匪浅。本人自然没有这些大牛的实力,也不敢班门弄斧的进行所谓的深度剖析。自己的一个项目目前正在使用MVC,自然会有一些对应的代码和小窍门,于是规整了一下发表出来。一是可以让大家在使用MVC的时候有个捷径,二是自己总结,三是看看大家有什么看法和建议。

今天开篇第一个,不知道要写多少,也不知道能写多少。没有给自己定什么目标。虽然曾经和AnyTao说要多写点Blog混个MVP当当,至少Windows 7出的时候还能有个正版的号(寒自己一个 - -!!!),但是平心而论,自己还真没有到达MVP的境界。

废话说了很多,说说这篇文章吧。ModelBinder大家应该用了很多,特别是在Post Action函数里面绑定复杂的View Model的时候非常好用。微软自带的DefaultBinder几乎可以满足我们所有的要求了,但是在开发的时候发现有个需求,就是有些页面有上传文件的功能,而默认的ModelBinder似乎还不支持,于是就自己做了个ModelBinder。代码非常简单,如下。

    public class UploadFilesModelBinder : IModelBinder
    {
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            // Default binding the normal properties.
            var defaultBinder = new DefaultModelBinder();
            var model = defaultBinder.BindModel(controllerContext, bindingContext);
            // Bind the image.
            var files = controllerContext.HttpContext.Request.Files;
            foreach (var property in bindingContext.ModelType.GetProperties())
            {
                if (property.PropertyType == typeof(IHttpPostedFile))
                {
                    // Get the corresponding property name which type is IHttpPostedFile.
                    var propertyName = property.Name;
                    var file = files.Get(string.Format("{0}.{1}", bindingContext.ModelName, propertyName));
                    if (file == null)
                    {
                        file = files.Get(propertyName);
                    }
                    // Set the image into the property.
                    if (file != null)
                    {
                        var fileWapper = new RequestPostedFileWrapper(file, controllerContext.HttpContext.Server);
                        property.SetValue(model, fileWapper, null);
                    }
                }
            }
            return model;
        }
    }

首先调用系统自己的DefaultModelBinder把普通的属性值绑定进去。然后开始绑定上传的文件(由于项目主要是上传图片,所以注释写的是‘图片’但是可以支持任意文件)。通过反射把当前Model里面的所有类型是IHttpPostedFile(稍后会解释这个接口)的属性取出来,然后通过属性名寻找Request.Files里面有没有对应的文件名(可以直接就是属性名,或者是[Model名].[属性名])。如果找到了,则实例化RequestPostedFileWrapper类然后设定到属性上面。

使用起来还算简单,比如我们现在有一个Product Creation页面,需要用户输入一些Product信息的同时上传一个Product的图片,那么我们的ViewModel可能是这样的。

    public class StockProductCreateModel : ModelBase
    {
        public KeyValuePair<int, string> Category { get; set; }

        [Required(ErrorMessage = "Cateogry ID is mandatory.")]
        public int CateogryID { get; set; }

        [Required(ErrorMessage = "Name is mandatory.")]
        public string Name { get; set; }

        [Required(ErrorMessage = "Description is mandatory.")]
        public string Description { get; set; }

        public IHttpPostedFile MainImage { get; set; }
    }

我们的图片就是定义为Public IHttpPostedFile MainImage {get; set;}这个属性。相对应的只需要在我们的View的Form里面加入一个文件上传元素就可以了。

        <% using (Html.BeginForm("ProductCreate", "Stock", FormMethod.Post, new { enctype = "multipart/form-data" }))
           { %>
        <%= Html.Hidden("model.CateogryID", Model.Category.Key)%>
        <p>
            <%= Html.Label("model.Name", "Product Name:")%>
            <%= Html.TextBox("model.Name", Model.Name, new { style = "width: 80%;" })%>
            <%= Html.ValidationMessage("model.Name")%>
        </p>
        <p>
            <%= Html.Label("model.Description", "Description:")%>
            <%= Html.TextArea("model.Description", Model.Description, new { style = "width: 80%;" }) %>
            <%= Html.ValidationMessage("model.Description")%>
        </p>
        <p>
            <%= Html.Label("model.MainImage", "Main Image:")%>
            <input name="model.MainImage" type="file" />
        </p>
        <p>
            <%= Html.SubmitImage("Submit", "~/Content/submit.png")%>
            <%= Html.BackImage("/Content/back.png")%>
        </p>
        <% } %>

只需要<input type=”file” />这个元素的name属性值和刚才我们定义的Property的名字对应就可以了,比如这里定义为model.MainImage就是首先用ViewModel实例的名字(通过Controller传进来的参数名)然后是属性名;或者可以直接定义为属性名(MainImage)。

最后在Controller里面显示声明我们要用这个UploadFilesModelBinder进行数据绑定就可以把上传的文件绑定到我们的Model里面了。

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult ProductCreate([ModelBinder(typeof(UploadFilesModelBinder))] StockProductCreateModel model)
        {
            throw new NotImplementException();
        }

最后谈到了这个IHttpPostedFile接口以及相对应的RequestPostedFileWrapper实现类。加入这个接口主要是分层的考虑。如果按照简单的三层架构来设计,一般分为表现层(网站所在的层)、业务层(基本上所有的业务逻辑)和数据层(和数据库进行交互),而这三层应该是相互独立的,我的理解就是业务层应该不依赖于表现层和数据层使用什么技术。比如现在我们的例子里面,业务逻辑就是“新建一个Product的时候保存数据的同时可以为他上传一个图片”。业务层就是要实现这个逻辑,但是他不能确定也不应该确定这个图片是怎么来的(通过网页上传进来的)以及怎么保存的(保存在服务器的某个目录下)。所以我们建立了这个接口IHttpPostedFile用来隔离和Web相关的操作来保证业务层的“纯洁”。

    public interface IHttpPostedFile
    {
        bool IsAvailable { get; }
        string FileName { get; }
        void SaveAs(Size normalSize, Size thumbnailSize);
    }

接口的内容非常简单,IsAvailable表示了这个文件是不是有效的,FileName表示文件名,可以用于保存到数据库的响应字段比如ImagePath,SaveAs方法则负责将文件(图片)保存。这里由于要支持图片的尺寸重定义操作,所以提供了两个参数用来指示普通尺寸和缩略图尺寸。当然如果把这个接口修改为支持任意文件,则可以取消这两个参数。

这样在业务层进行保存操作的时候,只需要对这个接口进行操作就可以了,业务层完全不知道这个接口的实现类是什么样子的,自然也就隔离了实现的方法。比如

            // Save the main image if specified.
            if (model.MainImage.IsAvailable)
            {
                // Insert the image record.
                var image = new ProductImages();
                image.Products = product;
                image.ImagePath = model.MainImage.FileName;
                image.Description = model.MainImageDescription;
                image.EnteredDate = DateTime.Now;
                image.UpdatedDate = DateTime.Now;
                image.IsDeleted = false;
                Resolve<IProductImagesRepository>().InsertEntity(image);
                // Save the image file.
                model.MainImage.SaveAs(Resolve<ISettingService>().ImageNormalSize, Resolve<ISettingService>().ImageThumbnailSize);
                // Update the product record set the main image.
                product.MainImage = image;
                product.UpdatedDate = DateTime.Now;
                Resolve<IProductsRepository>().UpdateEntity(product);
            }

通过model.MainImage.FileName将文件名保存到数据库的ImagePath字段,然后通过model.MainImage.SaveAs保存图片本身。对于这个业务层中的函数,完全不知道这个图片的来源(显示层)和保存的方法(数据层)。

最后为了能让在显示层的文件上传对象传递进业务层,需要一个实现了IHttpPostedFile接口的类,我是通过RequestPostedFileWrapper这个类来实现的。它的内部使用了HttpPostedFileBase这个定义在System.Web下面的类来保存上传的文件对象,同时实现了IHttpPostedFile接口并使用HttpPostedFileBase的保存等操作来实现接口的方法体。具体代码比较长因为我还实现了图片变换尺寸的功能。

 

    public class RequestPostedFileWrapper : IHttpPostedFile
    {
        public enum ImageType : int
        {
            Original = 0,
            Normal = 1,
            Thumbnail = 2
        }

        private HttpPostedFileBase _file;
        private HttpServerUtilityBase _server;
        private string _fileName;

        public bool IsAvailable
        {
            get
            {
                return _file != null && _file.ContentLength > 0 && !string.IsNullOrEmpty(_file.FileName);
            }
        }

        public string FileName
        {
            get
            {
                return _fileName;
            }
        }

        public static string GetRelevantPath(ImageType type, string fileName)
        {
            return Path.Combine(Settings.ImageRoot, Path.Combine(type.ToString().ToLower(), fileName));
        }

        private string GetServerMappedPath(ImageType type)
        {
            return _server.MapPath(GetRelevantPath(type, FileName));
        }

        private Image GetResizedImage(Image originalImage, int width, int height)
        {
            Image ret = null;
            var originalWidth = originalImage.Width;
            var originalHeight = originalImage.Height;
            var rateWidth = (double)width / (double)originalWidth;
            var rateHeight = (double)height / (double)originalHeight;
            var rate = Math.Min(rateWidth, rateHeight);
            if (rate >= 1)
            {
                // The target size is bigger than the input image's size so no need to resize.
                ret = originalImage;
            }
            else
            {
                // The target size is smaller than the input image's size so resize the input image and returned.
                ret = new Bitmap(originalImage, (int)Math.Ceiling(originalWidth * rate), (int)Math.Ceiling(originalHeight * rate));
            }
            return ret;
        }

        public void SaveAs(Size normalSize, Size thumbnailSize)
        {
            Image originalImage = Image.FromStream(_file.InputStream);
            var originalPath = GetServerMappedPath(ImageType.Original);
            // Save the normal file.
            var normalPath = GetServerMappedPath(ImageType.Normal);
            Image normalImage = GetResizedImage(originalImage, normalSize.Width, normalSize.Height);
            normalImage.Save(normalPath);
            // Save the thumbnail file.
            var thumbnailPath = GetServerMappedPath(ImageType.Thumbnail);
            Image thumbnailImage = GetResizedImage(originalImage, thumbnailSize.Width, thumbnailSize.Height);
            thumbnailImage.Save(thumbnailPath);
        }

        private void Initialize(HttpPostedFileBase file, string fileName, HttpServerUtilityBase server)
        {
            _file = file;
            _fileName = fileName;
            _server = server;
        }

        public RequestPostedFileWrapper(HttpFileCollectionBase files, HttpServerUtilityBase server, string fileName)
        {
            if (files != null && files.Count > 0)
            {
                Initialize(files[0], fileName, server);
            }
            else
            {
                Initialize(null, null, null);
            }
        }

        public RequestPostedFileWrapper(HttpPostedFileBase file, HttpServerUtilityBase server)
        {
            Initialize(file, Guid.NewGuid().ToString() + System.IO.Path.GetExtension(file.FileName), server);
        }

        public RequestPostedFileWrapper(HttpFileCollectionBase files, HttpServerUtilityBase server)
        {
            if (files != null && files.Count > 0)
            {
                Initialize(files[0], Guid.NewGuid().ToString() + System.IO.Path.GetExtension(files[0].FileName), server);
            }
            else
            {
                Initialize(null, null, null);
            }
        }
    }

其实这种实现方法也不是我的原创,参考了Scott GuPhil Haack等人关于怎么实现ValidationDictionary的方法。他们就是通过一个IValidationDictionary来将本属于表现层的ModelState传递进业务层,但是让业务层脱离对于System.Web.Mvc的依赖。也不知道这是个什么“模式”,但是发现用起来还很方便。

最后照例来点总结吧,虽然都是废话。第一篇写MVC的东西,写着自己都心虚,生怕写出什么贻笑大方的来。几年前刚才博客园,因为在CSDN的VB.NET版混了一个星星出来,还得到了水如烟的鼓励,顿时飘飘然觉得自己是高手了,于是就大肆发帖。但是后来随着接触的人越来越多,发现高人实在是太多了,于是由狂妄自大变成了谨小慎微,什么都不敢发了——就连在牛人的Blog里面回复都要深思熟虑一番。这一次鼓足勇气再次发文,完全是和AnyTao的一次对话让我有了些新的认识。

这篇文章主要说了说我在项目中是怎么使用ModelBinder和一些其他相关的手法解决MVC中上传文件的问题。MVC的好处是扩展点很多,而且很方便。做一个好框架不容易,做一个易于扩展的框架更是困难。MVC让我觉得很成功,同时期待MVC2的到来。

posted @ 2009-09-01 13:45 妖居 阅读(503) 评论(1) 编辑
摘要: There are so many reason when we've to migrate our base-end database from MsSql to MySql. But I think the most one is the price. Since MySql is NOT free if you do not use it to develop a software under the GPL, but the cost of using MySql is much lower than MsSql. This is the same situation I've just met.
There are many diffrences between MsSql and MySql so we need spend more time to investigate, research and trail migration.阅读全文
posted @ 2007-09-18 15:40 妖居 阅读(1162) 评论(0) 编辑
摘要: Windows Workflow Foundation(以下简称WWF)为我们提供了一种定义工作流的方式,依靠Visual Studio Orcas Beta 1(以下简称VS9),可以通过图形界面定义一个业务的工作流程,进而在代码页实现工作流的工作细节。使用工作流的时候只需要简单的启动这个流程就可以了。如果工作流发生了变化,那么只需要修改工作流Project里面的流程或者实现,对于工作流的使用者来说不用修改代码。阅读全文
posted @ 2007-06-28 11:18 妖居 阅读(3190) 评论(6) 编辑
摘要: 在Artech的我的WCF之旅(6)文章中向我们介绍了Windows Application在使用Duplex Service的时候,会出现Deadlock。并且Artech还想我们介绍了两种解决办法:使用IsOneWay修饰Service Contract Operator;在Client端使用新的线程调用Service的Method。本文继续考虑这个问题的解决方法,在Service和Clien...阅读全文
posted @ 2007-06-21 17:13 妖居 阅读(1896) 评论(3) 编辑
摘要: 1、 将字节数组转化为数值public static int ConvertBytesToInt(byte[] arrByte, int offset){ return BitConverter.ToInt32(arrByte, offset);}2、 将数值转化为字节数组第二个参数设置是不是需要把得到的字节数组反转,因为Windows操作系统中整形的高低位是反转转之后保存的。public sta...阅读全文
posted @ 2007-06-15 09:40 妖居 阅读(1274) 评论(1) 编辑
摘要: 发布一个小软件。可以编辑固定长、CSV文件的表格化编辑器。所谓固定长文件,就是由若干固定长度的Record组成的文件,一个Record里面的项目按照字节的位置进行划分。项目内容可以是字符串、数字或二进制数值。CSV文件就不多说了。具体的功能,请访问iMatrixitor发布专用页。由于是个人制作的小软件,所以没有经过全面的测试。同样由于是个人在业余时间制作的小软件,所以未注册的版本屏蔽了保存、打印...阅读全文
posted @ 2007-06-07 22:51 妖居 阅读(2912) 评论(2) 编辑
摘要: 面向下一代的Visual Studio,Visual Basic和C#语言都有了相应的改进。其中最为主要的就是LINQ(Language Integrated Query)的引入。简单来说,LINQ就是把查询语言整合到了代码当中,而且将查询的对象从数据库扩展到所有的可迭代的任何数据结构上面。
本文主要翻译了Microsoft .NET LINQ Preview (May 2006)里面的《Getting Started With LINQ in Visual Basic》这篇文章。原文包含15部分,本译文删除掉了前面的“Introduction”以及后面的“Additional samples”两部分。同时加入了译者在翻译的同时发现的一些问题以及自己的一些想法。
本人很少翻译文章,而且LINQ目前来讲算是比较前沿的技术,在很多的名词和句子方面组织的还不够好,有的译法还欠推敲。译注里面的一些观点只是本人学习中的一些想法,只代表个人观点。希望大家多提意见。阅读全文
posted @ 2007-02-13 16:14 妖居 阅读(2286) 评论(9) 编辑
摘要: 插件功能给软件的使用者可以扩充软件功能的机会。我们不可能让软件适用于所有人,也不是所有的人都会出资帮助你实现他们的需求。插件功能提供了一个软件的高度可扩充性,允许用户作为软件的二次开发者,继续完善软件的功能。
本文通过模仿Visual Studio .Net Addin的实现方法来实现一个提供插件功能的软件。包括提供插件的软件制作、插件的制作一个相关的技术知识。阅读全文
posted @ 2007-01-08 17:28 妖居 阅读(2694) 评论(18) 编辑
摘要: DataGridView虽然好用,但是如果数据量比较大的话就会出现性能的问题。网上提供的一般做法是通过虚拟模式(Virtual Mode)来加速。但是有的时候,可以通过简单的设置来加速DataGridView。(1)使用绑定数据。就是通过加载DataTable然后通过BindingSource来和DataGridView.DataSource进行交互。(2)不要使用AutoSizeColumnsM...阅读全文
posted @ 2006-12-12 10:47 妖居 阅读(1983) 评论(3) 编辑