上一主题下一主题
«12345»Pages: 1/14     Go
关键字
主题 : 实例快速上手 -ASP.NET 4.5新特性WebAPI从入门到精通
级别: VIP四级

UID: 230365
精华: 12
发帖: 314
威望: 3432 点
学点: 380 点
贡献: 2 点
好评: 1 点
学币: 0 个
注册时间: 2010-04-16
最后登录: 2014-09-25
楼主  发表于: 2014-06-16 12:25||

0 实例快速上手 -ASP.NET 4.5新特性WebAPI从入门到精通

管理提醒: 本帖被 zylzyl 执行加亮操作(2014-06-16)
EyHL&  
h@J3+u<  
}?MbU6"  
n4.\}%=z  
"whs?^/  
.实例快速上手 -ASP.NET 4.5新特性WebAPI从入门到精通 /N,\st  
7jGfQ  
     在新出的MV****中,增加了WebAPI,用于****REST风格的WebService,新生成的WebAPI项目和典型的MVC项目一样,包含主要的Models、Views、Controllers等文件夹和Global.asax文件。Views对于WebAPI来说没有太大的用途,Models中的Model主要用于保存Service和Client交互的对象,这些对象默认情况下会被转换为Json格式的数据迚行传输,Controllers中的Controller对应于WebService来说是一个Resource,用于****服务。和普通的MVC一样,Global.asax用于配置路由规则。 7>F[7_  
环境准备 ) )t]5Ys%;  
     建议使用VS2012以上版本创建WebAPI,如果是使用VS2010,需要安装VS2010 SP1升级包,MV****升级包,打****VS2012创建如下: J&B5Ll  
第一步:新建ASP.NET Web应用程序 ?!.J 0q  
  [attachment=7091] ,$U~<Zd  
第二步:建议WebAPI mQ9shdvt-  
  [attachment=7092] <9c{Kt.5(  
新生成的WebAPI项目和典型的MVC项目一样,包含主要的Models,Views,Controllers等文件夹和Global.asax文件 >CwI(vXn  
  [attachment=7093] $LcMG,8%_  
注意:再次强调Views对于WebAPI来说没有太大的用途,Models中的Model主要用于保存Service和Client交互的对象,这些对象默认情况下会被转换为Json格式的数据进行传输,Controllers中的Controller对应于WebService来说是一个Resource,用于****服务。和普通的MVC一样,Global.asax用于配置路由规则(二)Models = J]M#6N0  
和WCF中的数据契约形成鲜明对比的是,MVC WebAPI中的Model就是简单的POCO,没有任何别的东西,如,你可以创建如下的Model PDLps[a  
public class UserModel '8FHn~F  
{ r]]:/pw?t  
        public int Id { get; set; } Aj(y]p8  
        public string UserName { get; set; } :Ys ;)W+R  
        public string PassWord { get; set; } KTwP.!<v  
} cob??|,\m  
   注意:Model必须****public的属性,用于json或xml反序列化时的赋值 8k+k\V{  
(三)Controllers Tt=;of{  
MVC WebAPI中的Controllers和普通MVC的Controllers类似,不过不再继承于Controller,而改为继承API的ApiController,一个Controller可以包含多个Action,这些Action响应请求的方法与Global中配置的路由规则有关,在后面结束Global时统一说明 @VyNe(U  
mx#)iHY  
(四)Global DghqSL ^s  
默认情况下,模板自带了两个路由规则,分别对应于WebAPI和普通MVC的Web请求,默认的WebAPI路由规则如下 Zv* uUe  
1            routes.MapHttpRoute( <GLoTolZ  
2                 name: "DefaultApi", <fA}_BH%]  
3                 routeTemplate: "api/{controller}/{id}", 0QxBC7` qp  
4                 defaults: new { id = RouteParameter.Optional } if3z Fh  
5             ); PWquu`  
可以看到,默认路由使用的固定的api作为Uri的先导,按照微软官方的说法,用于区分普通Web请求和WebService的请求路径: 5=}CZYWB  
可以看到,默认的路由规则只指向了Controller,没有指向具体的Action,因为默认情况下,对于Controller中的Action的匹配是和Action的方法名相关联的:具体来说,如果使用上面的路由规则,对应下面的Controller: Sz]1`%_H/  
public class UserController : ApiController 7#X`D  
    { .F[5{XV  
        public  List<UserModel> allModeList = new List<UserModel>() { <I0om(P  
          new UserModel(){ Id=1,UserName="zhang", PassWord="123"}, DF'~ #G8  
          new UserModel(){ Id=2,UserName="lishi", PassWord="123456"}, 4/v[ .5  
          new UserModel(){ Id=3,UserName="wang", PassWord="1234567"} 9l:[jsk<d  
        }; 8*|*@  
        //Get  api/User/ *)oBE{6D  
        public IEnumerable<UserModel> GetAll() >6IUle>z  
        { :LC3>x`:  
            return allModeList; {xTh!ih2 -  
        } Cv4nl7A'  
        //Get api/User/1 /CbiYm  
        public IEnumerable<UserModel> GetOne(int id) { 6&L;Sw#Dg  
            return allModeList.FindAll((m) => { return m.Id == id; }); P m&^rC;  
        }  =WEDQ\ c  
        //POST api/User/ %*Vr}@BA)  
        public bool PostNew(UserModel user) M a3}w-=;  
        { 8o.|P8%  
            try dP>FXgY  
            { _'^_9u G  
                allModeList.Add(user); Vs Z7 n~e  
                return true; H\a\xCP3  
            } }wI +e Mr  
            catch { VN$7r  
                return false; ,p!IFS`  
            } 7(P4KvkI  
        } G|||.B 8  
        //Delete api/User/ 6z:/ma^  
        public int DeleteAll() {.DY\;Q  
        { bqaj ~:}@  
            return allModeList.RemoveAll((mode) => { return true; }); 8U7d d[  
        } )HvB ceN  
        //Delete api/User/1 rhly.f7N=A  
        public int DeleteOne(int id) { LB9W.cA   
            return allModeList.RemoveAll((m) => { return m.Id == id; }); c\O2|'JzE  
        } Z`zLrXPD)  
        //Put api/User d>Nh<PqH6  
        public int PutOne(int id, UserModel user) %?[0G,JG  
        { {) sE;p-  
            List<UserModel> upDataList = allModeList.FindAll((mode) => { return mode.Id == id; }); 7r.~L  
            foreach (var mode in upDataList) /-WmOn*  
            { @D `j   
                mode.PassWord = user.PassWord; nV`W0r(f'  
                mode.UserName = user.UserName; _+~&t9A!  
            } JZE@W -2  
            return upDataList.Count; ;18u02z^  
        } Ww#!-,*]o  
} cqW(9A|8  
   则,会有下面的对应关系: !41"`D!1  
   URL       HttpMethod   对应的Action名 GCv1x->  
   /api/User      GET        GetALL Pf?15POg&B  
   /api/User/1    GET         GetOne t~) g)=>  
   /api/User      POST        PostNew ]<c\+9  
   /api/User/1    DELETE     DeleteOne <U\8&Uv>  
   /api/User      DELETE     DeleteALL "V`DhOG&  
   /api/User      PUT         PutOne T=@Ygjk  
'* /$66|  
客户端JS调用 ,ei=w,O  
   function getAll() { QXl~a%lB  
        $.ajax({ z^WY5~?  
            url: "api/User/", jpoNTl'  
            type: 'GET', i'^! SEt  
            success: function (data) { vg _PMy\  
                document.getElementById("modes").innerHTML = ""; 8s-X H  
                $.each(data, function (key, val) { @U1t~f^  
                    var str = val.UserName + ': ' + val.PassWord; 6E^9>  
                    $('<li/>', { html: str }).appendTo($('#modes')); )ZFc5m^+u  
                }); J(= y$8xje  
            } _9Rj,  
        }).fail( 1rLxF{,  
        function (xhr, textStatus, err) { .f>7a;V?}  
            alert('Error: ' + err); NS*Lv  
        }); uss!E!_%,  
   } imhE=6{  
function find() { [qk c6sqo  
        $.ajax({ +RkXe; q  
            url: "api/User/1" , Pt-O1$C[  
            type: 'GET', wqJl[~O$  
            success: function (data) { 4%JJ} {Ff  
                document.getElementById("modes").innerHTML = ""; =ReS lt  
                $.each(data, function (key, val) { _g,_G  
                    var str = val.UserName + ': ' + val.PassWord; BqdpJIr  
                    $('<li/>', { html: str }).appendTo($('#modes')); $'<$:;4b3  
                }); *m`x/_y+  
            } 7+hc?H[&'  
        }).fail( Ae1b`%To  
        function (xhr, textStatus, err) { U^qS[H M  
            alert('Error: ' + err); OIjG`~Rx  
        }); _?$w8 S%  
    } a( N;| <  
;z+}|>!  
t\Qm2Q)>  
    function add() { zTi 8y<}  
gi)C5 J4  
        $.ajax({ ,6"[vb#*3  
            url: "api/User/", %e|UA-(  
            type: "POST", +OtD@lD`!  
            dataType: "json", 1Oak8 \G  
            data: { "Id":4,"UserName": "admin", "PassWord": "666666"}, dX[ Xe  
            success: function (data) { r/HG{XH`  
                getAll(); y$6EEp  
            } :/RvtmW  
        }).fail( ng6E &<Z  
        function (xhr, textStatus, err) { uigzf^6,  
            alert('Error: ' + err); K 6,c||#<  
        }); .SSPJY(  
V!e*J ,g  
    } 54RexB o  
C]ax}P>BQ  
    function removeUser() { 7;?7q  
        $.ajax({ ]5MT-qU  
            url: "api/User/3", Mt%Q5^  
            type: 'DELETE', Qkw_9  
            success: function (data) { q 1u_ r  
                document.getElementById("modes").innerHTML = ""; Kf)$/W4  
                getAll(); u~mpZ"9$ 3  
            } |O"Pb`V+  
        }).fail( {e0aH `me  
        function (xhr, textStatus, err) { 'kg~#cf/+  
            alert('Error: ' + err); F:.8O ,%u  
        }); 7~QAprwVS  
    } /^WawH6)6  
ww'B!Ml>F  
    function removeAll() { [g+WL\1  
        $.ajax({ [RTo[-ci2  
            url: "api/User/", e%0IE X  
            type: 'DELETE', 6QPT  
            success: function (data) { x@> ~&eP  
                document.getElementById("modes").innerHTML = ""; zNEN[  
                getAll(); qi8AK(v  
            } \oP  
        }).fail( M,Lq4bz  
        function (xhr, textStatus, err) { &@PAv5iNf  
            alert('Error: ' + err); j1 ap,<\.k  
        }); Ct3+ga$  
    } ER4#5gd  
    function udpate() { vQDR;T"]  
        $.ajax({ R F;u1vEQ8  
            url: "api/User/1", :`4L V  
            type: 'PUT', $u)#-X;x  
            dataType: "json", KT<N ;[;  
            data: { Id: 1, "UserName": "admin", "PassWord": "666666" }, Xxm7s S  
            success: function (data) { a3He-76  
                document.getElementById("modes").innerHTML = ""; %r:4'$E7|  
                getAll(); I7<UC{Ny  
            } oV~S4 |9:  
        }).fail( g+C~}M_7  
        function (xhr, textStatus, err) { (W9 K: ]}  
            alert('Error: ' + err); w&&)v~Y_  
        }); X: Be'  
    } a]I~.$G   
这样就实现了最基本的CRUD操作。 C"h7'+Kw  
扩展需求 @\=4 Rin/q  
问题1:我想按照用户名称(UserName)进行查询,怎么****? !B#tJD  
****法:第一步:在UserController类中加一个方法名称叫:GetUserByName,如下所示: "YV vmCp  
public UserModel GetUserByName(string userName) { ',6d0>4 *  
            return allModeList.Find((m) => { return m.UserName.Equals(userName); }); SQJ4}w>i  
} R ggZ'.\  
第二步:在客户端index.cshtml中调用 &Hl w2^  
    function getUserByName() { +n|@'= ]  
        $.ajax({ yJ 8_<A  
            url: "api/User/zhang", TZObjSm_v  
            type: 'GET', 9['>$ON  
            success: function (data) { 2j[; M-3  
                document.getElementById("modes").innerHTML = ""; cvV8 ;  
                var str = data.UserName + ': ' + data.PassWord; m khp@^5  
                $('<li/>', { html: str }).appendTo($('#modes')); _+&/P&  
            } 08nA}+k  
        }).fail( "s\himoa  
        function (xhr, textStatus, err) { H*&!$s.  
            alert('Error: ' + err); VS5D)5w#  
        }); NF_[q(k'  
   } mFBuKp+0)h  
如果URL是: url: "api/User/zhang",将会报错:Bad Request #23($CSE  
原因是他会自动调用我们的GetOne(int id) 这个方法,类型转换出错 Mbtk:GuY  
解决****法: m =MM  
改变URL为: url: "api/User/?userName=zhang", f5#VU7=1F2  
问题2:我想按用户名称(UserName) 和用户密码(PassWord)一起来进行查询,怎么****? t\-;n:p -  
解决****法 XJ?zP=UK  
第一步:UserController类中,可以重载一个GetUserByName的方法,如下所示: s&6/fa  
    public UserModel GetUserByName(string userName) { q>VvXUyK,  
            return allModeList.Find((m) => { return m.UserName.Equals(userName); }); 51!#m|  
        } ->&amPv  
第二步:客户端调用: ztV%W6  
function getUserByName() { / 6#i$\ j  
        $.ajax({ \Jr7Hy1;  
            url: "api/User/?userName=zhang&passWord=123", //这里尤其需要注意 o 0H.DeP  
            type: 'GET', ajD/)9S  
            success: function (data) { }9Awv#+  
                document.getElementById("modes").innerHTML = ""; 6b h.5|  
                var str = data.UserName + ': ' + data.PassWord; +Pb@@C&  
                $('<li/>', { html: str }).appendTo($('#modes')); Y r 1k\q  
            } X"v)9 p  
        }).fail( mUwGr_)wj  
        function (xhr, textStatus, err) { !]!J"!xg*  
            alert('Error: ' + err); bN.U2%~!  
        }); ;C+ _KS  
   } =tqChw   
\Oa11c`6  
路由规则扩展  =E :a\r  
和普通的MVC一样,MVC WebAPI支持自定义的路由规则,如:在上面的操作中,路由规则使用 VKHzGfv  
"api/{controller}/{id}" L7$1rO<  
则限定了使用GET方式利用URL来传值时,controller后面的接****参数名为id,但是在Controller中,如果GetOne方法的接****参数名为key,是不会被匹配的,这是只需要新增一个新的路由规则,或修改原先的路由规则为: 5OHF=wh  
"api/{controller}/{key}",如下所示: O*hQP*Rs  
config.Routes.MapHttpRoute( @s~*>k#"#  
                name: "DefaultApi", Ve\P,.  
                routeTemplate: "api/{controller}/{key}", =_,j89E  
                defaults: new { key = RouteParameter.Optional } _l&.<nz  
            ); 2I1CKA:7g  
当然,可以对路由进行更深的扩展,如:扩展成和普通MVC一样的路由: C 4hvk'=  
"api/{controller}/{action}/{id}" lxOUV?m^N  
这样,就要求同时使用Action和HTTP方法进行匹配当然,根据微软的说法,这种使用是不被****的,因为这不符合大家对WebService的一般认知: p(x<h  
使用Attribute声明HTTP方法 420yaw/":  
       [HttpGet] -n`2>L1  
       public IEnumerable<TestUseMode> FindAll() NLO&.Q]#  
       [HttpGet] %1%@L7wP>  
       public IEnumerable<TestUseMode> FindByKey(string key) &W-1W99auE  
       [HttpPost] q%8Ck)xz  
       public bool Add(TestUseMode mode) 9c=`Q5  
       [HttpDelete] ;2iDa  
       public int RemoveByKey(string key) ?E.MP7Y# V  
       [HttpDelete] $O^U"  
       public int RemoveAll() {DbWk>[DkG  
       [HttpPut] )TyI~5>;  
       public int UpdateByKey(string key, string value) BYWs\6vK  
       [NonAction]   WOuk> /  
       public string GetPrivateData() 9Gk#2  
当然,我只列出了方法名,而不是这些方法真的没有方法体...方法体是不变的,NoAction表示这个方法是不接****请求的,即使以GET****头。如果感觉常规的GET,POST,DELETE,PUT不够用,还可以使用AcceptVerbs的方式来声明HTTP方法,如: _f<#+*y  
[AcceptVerbs("MKCOL", "HEAD")] -3&m gd  
public int UpdateByKey(string key, string value) wVtBH _>  
{ u eV,p?Wo  
            List<TestUseMode> upDataList = allModeList.FindAll((mode) => { if (mode.ModeKey == key) return true; return false; }); g2W ZW#a)  
            foreach(var mode in upDataList) S]}W+BF3  
            { `NARJ9M   
                mode.ModeValue = value; wb/@g=` d  
            } m15> ^i^W  
            return upDataList.Count; Ls(l  
} 2, r{z J8  
附:什么是REST风格? 参考:什么是REST风格 lxXIu8  
http://hi.baidu.com/yankaiwei/item/1f0b37dd922d53ef3cc2cb69 R)BH:wg"  
第二部分:综合示例:应用ASP.NET MV****+WebAPI+FluentData****发Web应用 cK~VNzsz  
第一步:创建数据库 U~Y jTjbd  
NorthWind数据库的Customers表 95hdQ<W  
Create DataBase NorthWind 53)*i\9&  
Go DyPb]Udb:  
Use NorthWind V.Qy4u7m  
Go ,ku3;58O<  
CREATE TABLE [dbo].[Customers]( /^9yncG;>  
    [CustomerID] [nchar](5) NOT NULL, %~^:[@xa*  
    [CompanyName] [nvarchar](40) NOT NULL, BF+i82$zo  
    [ContactName] [nvarchar](30) NULL, - <M'h  
    [ContactTitle] [nvarchar](30) NULL, W`` -/  
    [Address] [nvarchar](60) NULL, K:8. Dvn  
    [City] [nvarchar](15) NULL, B*T;DE   
    [Region] [nvarchar](15) NULL, g,d'&r"JWt  
    [PostalCode] [nvarchar](10) NULL, wQw y+S  
    [Country] [nvarchar](15) NULL, Q"b62+03  
    [Phone] [nvarchar](24) NULL, |g8Q.*"l[  
    [Fax] [nvarchar](24) NULL, p-,(P+Np  
CONSTRAINT [PK_Customers] PRIMARY KEY CLUSTERED 7 Hzv-s  
( Onj)AJ9M0r  
    [CustomerID] ASC o `}(1$a>  
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ,1|0]:  
) ON [PRIMARY] TOXZl3 s5#  
vD p|9VY?  
GO n>i}O!agg  
第二步:创建 FluentData.Entity层,创建Customer实体类 !0!r}#P  
namespace FluentData.Entity 7bC)Co#:   
{ d0 qc%. s  
    public class Customer m. "T3K  
    { i.G"21M  
        public string CustomerID { get; set; } U((mOm6  
        public string CompanyName { get; set; } *ci%c^}V  
        public string ContactName { get; set; } 5;Q9Z1 `  
        public string ContactTitle { get; set; } |tqYRWn0  
        public string Address { get; set; } bbxo!K m"  
        public string City { get; set; } 2!;U.+(  
        public string Region { get; set; } l"app]uVZ  
        public string PostalCode { get; set; } k~HS_b*]d  
        public string Country { get; set; }  -  j_  
        public string Phone { get; set; } pA?2UZ  
        public string Fax { get; set; } JPGEE1!B{b  
    } kH}HFl  
} BNpc-O~  
第三步:利用FluentData****数据的持久化 \+Pk"M  
首先引入FluentData.cs (见附件) HlC[Nu^6U  
其次:创建DBHelper类,代码如下: O(CmdSk,  
public class DBHelper DfV'1s4y  
    { Ab/KVB  
        public static IDbContext Context() { qF57T>v|  
            //return new DbContext().ConnectionString("server=127.0.0.1;uid=sa;pwd=sa;database=TestDB", new SqlServerProvider()); PWbi`qF)r  
            return new DbContext().ConnectionStringName("connString", new SqlServerProvider()); %"g; K  
        } YqNI:znm-  
   } gq[`g=x  
然后不要忘记修改ASP.NET MVC层所在的Web.config,加入数据库连结字符串: 0o &B 7N  
  <connectionStrings> <Pg<F[eDM  
    <add name="connString" connectionString="server=127.0.0.1;database=Northwind;uid=sa;pwd=sa;"/> bc(b1u?  
  </connectionStrings> U9]&~jR  
第三步:创建 CustomerService数据持久化类,代码如下: 'e5,%"5(c  
public class CustomerService m?_@.O@]  
    { %y_AT2A  
        private IDbContext context = DBHelper.Context(); "pW@[2Dkx/  
        public Customer Select(string customerId){ ->Bx>Y  
^dCSk==  
            return context.Select<Customer>("*").From("Customers").Where("CustomerID=@0").Parameters(customerId) 3P[u>xE  
                   .QuerySingle(); $G@^!(  
             {u_2L_  
        } tj;<EaM  
        public List<Customer> SelectAll() { >S=,ype~G  
            return context.Select<Customer>("*").From("Customers").QueryMany(); ]/y69ou  
        } #uHl  
ug. 'OR  
        public List<Customer> SelectAll(string sortExpression) { %x zgTZ  
            if (String.IsNullOrEmpty(sortExpression)) return null; @#W$7Gwf0  
            return context.Select<Customer>("*").From("Customers").OrderBy(sortExpression).QueryMany(); CKgbb4;<m[  
        } 3&ES?MyB#  
*, RxOz2=  
        public List<Customer> SelectAll(int currentPageIndex,int maxRows, string sortExpression) gg}^@h&?  
        { }$T!qMst{  
            var select = context.Select<Customer>("*").From("Customers"); 'p:L"L}Q?  
            if (maxRows > 0) { hDc)\vzr  
                if (currentPageIndex == 0) currentPageIndex = 1; Yvbk[Rb  
                select.Paging(currentPageIndex, maxRows); PZsq9;P$  
            } _z(ydL*  
            if (!string.IsNullOrEmpty(sortExpression)) { qc6eqE  
                select.OrderBy(sortExpression); 2W]y9)<c  
            } vspub^;5\  
 }j /r  
            return select.QueryMany(); ?B`Yq\L)  
        } Yt% E,U~g  
}R]^%q@&  
        public int CountAll() { [d~ 25  
            return context.Sql("select count(*) from Customers").QuerySingle<int>(); J4;F k  
        } maopr$r  
NDaM;`  
        public int Insert(Customer customer) { |~I-  
            return context.Insert<Customer>("Customers", customer).Execute(); |L"!^Y#=D  
        } [6Nw)r(a(  
o I6o$C  
        public int Update(Customer customer) { FTfejk!  
            return context.Update<Customer>("Customers", customer).Where("CustomerID", customer.CustomerID).Execute(); _2C[F~ +l  
        } V*U*_Y  
_hb@O2f  
        public int Delete(string customerId) { YN@ 4.&RP  
            return context.Delete("Customers").Where("CustomerID", customerId).Execute(); zz+p6`   
        } @xI:ZtM  
-n`igC  
        public int Delete(Customer customer) g@jAIy]  
        { \%!~pfM I  
            return this.Delete(customer.CustomerID);  MX j7Z3  
        } }`,}e259  
    } """gV)Y  
第四步:Web API,创建CustomerController 4mY(*2:HC  
注意要引用:FluentData.Entity及FluentData.DAL 程序集 LJ|2=lI+jb  
public class CustomerController : ApiController kr C4O2Fkj  
    { l`vb  
        private CustomerService customerService = new CustomerService(); Hi 1@  
        //Select All -K_p? l  
        public IEnumerable<Customer> Get() M=hH:[6 &  
        { y2U^7VrO  
            return customerService.SelectAll(); v{}i`|~J  
        } aJC,  
\ +cU}  
        //Select By Id &O' 6va  
        public Customer Get(string id) rR4_=S<Mi:  
        { >_Dq)n;%  
            return customerService.Select(id); YFVNkB O%  
        } !c+Nf2I7S  
DB'd9<  
        //Insert v: Av 2y  
        public void Post(Customer customer) hZE" 8%\q  
        { Gyak?.@R  
            customerService.Insert(customer); 9U_uw Rv2  
        } }hralef #N  
(RR:{4I  
Zr0bVe+h  
        //Update Zz<k^  
        public void Put(string id, Customer obj) FYI*44E  
        { :esHtkyML  
            customerService.Update(obj); i+$G=Z#3E  
        } ,Jy@n]x  
G[}$s7@k  
        //Delete Une,Y4{u  
        public void Delete(string id) X|}yp|  
        { 4!pMZ<$3  
            customerService.Delete(id); cnQ;6LtFTz  
        } Ak`7f$z  
   } ^tS{a*Yn  
第五步:View层代码 VQ5D?^'0/  
'u6T^YS  
namespace MyWebApI.Controllers )[d?&GK  
{ 2lVJ"jg  
    public class HomeController : Controller ~c&ygL3  
    { Yv?nw-HM  
        public ActionResult Index() 9`P<|(  
        { _g|zDi^  
            return View(); f}JiYZ  
        } S]E1+,-*  
Y}<w)b1e|  
        public ActionResult Test() { J*Dt\[X  
            return View(); .TcsXYL.`,  
        } kvWP[! j?)  
~*OQRl6F  
        public ActionResult CustomerManager() { .<0|V  
            return View(); Tm!pAD  
        } ]ow$VF{y  
    } [D !-~]5  
} Gk<M@d^hQ  
然后创建View =Q\z*.5j.  
=L,s6J8_'  
    <table id="customerTable" border="1" cellpadding="3"  style="width:700px"> #2`ST=#  
        <tr> }I3 ZNd   
            <th>Customer ID</th> ,t"?~Hl".  
            <th>Company Name</th> d']CBoK  
            <th>Contact Name</th> :{:R5d(_I  
            <th>Country</th> O*;$))<wX  
            <th>Actions</th> q1rBSlzN  
        </tr> ~ IPel  
        <tr> ]i$ <<u  
            <td><input type="text" id="txtCustomerId"  style="width:100px" size="5"/></td> 5k%Gj T  
            <td><input type="text" id="txtCompanyName"  style="width:150px" /></td> ( (.b&  
            <td><input type="text" id="txtContactName"  style="width:150px"/></td> 9T`$ gAI  
            <td><input type="text" id="txtCountry"  style="width:150px"/></td> D<V[:~ -o  
            <td><input type="button" name="btnInsert" value="Insert"  style="width:150px"/></td> MR=dQc  
        </tr> 9%{V?r]k  
    </table> 1L~y!il  
(8JL/S ;Z$  
<script type="text/j****ascript"> jXa;ovPK  
    $(function () { Alo;kt@x  
        $.getJSON("api/Customer", LoadCustomers);  c@eQSy  
    }); +c7e[hz  
x9QUo*MT  
    function LoadCustomers(data) { =1kE2u  
        $("#customerTable").find("tr:gt(1)").remove(); ; 9n}P@  
        $.each(data, function (key, val) { @2yoy&IO  
            var tableRow = '<tr>' + D8OW|wVE  
                            '<td>' + val.CustomerID + '</td>' + ":qhO0  
                            '<td><input type="text" value="' + val.CompanyName + '" /></td>' + hg2a,EU\Z  
                            '<td><input type="text" value="' + val.ContactName + '" /></td>' + sJI" m'r=Z  
                            '<td><input type="text" value="' + val.Country + '" /></td>' + 3I"xuKxc  
                            '<td><input type="button" name="btnUpdate" value="修改" /> <input type="button" name="btnDelete" value="****" /></td>' + _Wb3,E a=  
                            '</tr>'; '' Pu  
            $('#customerTable').append(tableRow); TnuaP'xZ  
        }); b0(bL_,  
Oax6_ kmOj  
        $("input[name='btnInsert']").click(OnInsert); A$JL"~R  
        $("input[name='btnUpdate']").click(OnUpdate); iBqxz:PHN(  
        $("input[name='btnDelete']").click(OnDelete); 7>-"r*W +z  
    } xYLTz8g=  
=qJlS b  
    function OnInsert(evt) { KbXENz&C  
        var customerId = $("#txtCustomerId").val(); Eo) #t{{  
        var companyName = $("#txtCompanyName").val(); d`w3I`P1  
        var contactName = $("#txtContactName").val(); kndN} Vq  
        var country = $("#txtCountry").val(); +A;AX.mr  
        var data = '{"CustomerID":"' + customerId + '","CompanyName":"' + companyName + '","ContactName":"' + contactName + '","Country":"' + country + '"}'; @cS(Bb!(M  
ov 'g'1}  
        $.ajax({ GG=R!+p2  
            type: 'POST', Fkvf[!Ci  
            url: '/api/Customer/', >)j`Q1Qc\  
            data: data, s7Z+--I)L  
            contentType: "application/json; charset=utf-8", {W' 9k  
            dataType: 'json', IaHu$` v  
            success: function (results) { "y7IH GJ\3  
                $("#txtCustomerId").val(''); .4cV X |T  
                $("#txtCompanyName").val(''); ?*/1J~<(@  
                $("#txtContactName").val(''); my}l?S[2d@  
                $("#txtCountry").val(''); &y+)xe:&S  
                $.getJSON("api/customers" + new Date().getTime(), LoadCustomers); 4/N{~  
                alert('添加成功!'); 7L4~yazmK  
            } ]r(&hqdR  
        }).fail( WNK)IC~c  
        function (xhr, textStatus, err) { 0F-%C>&g  
            alert('添加失败,原因如下: ' + err); PNB E  
        }); -}CMNh   
    } 8#ZF<B Y  
    function OnUpdate(evt) { V6!1(|  
'E)g )@^  
        var input; :>*0./hG  
        var customerId = $(this).parent().parent().children().get(0).innerHTML; ]j+J^g  
le1 50;7  
        input = $($(this).parent().parent().children().get(1)).find("input"); 1~ZFkcV_C  
        //input.removeAttr("disabled"); *%n(t+'q  
        var companyName = input.val(); L #`Vr$  
uwc@~=;  
        input = $($(this).parent().parent().children().get(2)).find("input"); W690N&Wz  
        //input.removeAttr("disabled"); _-: CU  
        var contactName = input.val(); pnp)- a*7  
{lbNYjknS  
        input = $($(this).parent().parent().children().get(3)).find("input"); nC5]IYL|  
        //input.removeAttr("disabled"); ly::?  
        var country = input.val(); )W^$7 Em  
x0||'0I0  
        var data = '{"CustomerID":"' + customerId + '","CompanyName":"' + companyName + '","ContactName":"' + contactName + '","Country":"' + country + '"}'; P{2j31u`  
         |lMc6C  
        $.ajax({ PB;j4  
            type: 'PUT', NJmyp!8  
            url: '/api/Customer/' + customerId, "V <WC"  
            data: data, 589P$2e1X  
            contentType: "application/json; charset=utf-8", @;7Ht Z`  
            dataType: 'json', [mFgo il  
            success: function (results) { Ns$,.D  
                $.getJSON("api/Customer" + new Date().getTime(), LoadCustomers); w ,-4A o2x  
                alert('修改成功 !'); qZ%0p*P#_  
            } ,!QtViA7  
        }).fail( Vx%!j&  
        function (xhr, textStatus, err) { 3oM&#a  
            alert('修改失败,原因如下: ' + err); &*; Z(ul&9  
        }); Qov*xRO6  
    } l{Xy %8  
    function OnDelete(evt) { ;~Gez;AhK  
        var customerId = $(this).parent().parent().children().get(0).innerHTML; W?yGV{#V(=  
        //var data = '{"id":"' + customerId + '"}'; vlo!D9zsV3  
        //var row = $(this).parent().parent(); 5Un)d<!7&u  
/Zg4JQ~  
        $.ajax({ tja7 y"(]  
            type: 'DELETE', `8kL=%(h  
            url: '/api/Customer/' + customerId, 3 (F+\4aRm  
            contentType: "application/json; charset=utf-8", >/'/^h  
            dataType: 'json', oO9 yI^  
            success: function (results) { W)Mc$`nX  
                $.getJSON("api/Customer?"+new Date().getTime(), LoadCustomers); i2!0bY  
                alert('成功****!'); gT2k}5d}p  
            } hw1J <Pl*  
        }).fail( {j%7/T{  
        function (xhr, textStatus, err) { %$F_oO7"  
            alert('****失败,原因如下: ' + err);  #zg"E<  
        }); S"%W^)mZ  
r#w.y g4EX  
    } @;Xa&*   
</script> B<jVo%og  
第三部分:Web API高级部分 XhmUtbs  
在第一部分和大家一起学习了建立基本的WebAPI应用,第二部分写了一个综合示例,立刻就有人想到了一些问题:1.客户端和WebService/WebAPI之间文件传输2.客户端或者服务端的安全控制要解决这些问题,要了解一下WebAPI的基本工作方式。 6*s:I&  
(一)WebAPI中工作的Class '~E=V:6  
在MVC中大家都知道,获取Request和Response使用HttpRequest和HttpResponse两个类,在WebAPI中使用两外两个类:HttpRequestMessage 和HttpResponseMessage,分别用于封装Requset和Response。除了这两个类之外,还有一个常见的抽象 类:HttpMessageHandler,用于过滤和****HttpRequestMessage和HttpResponseMessage tJpK/"R'  
d_ji ..T  
(二)解决第一个问题:客户端和WebService之间文件传输其 实第一个问题之所以被提出来应该是和客户端有关,如果客户端的请求是我们手写提交的,比如使用HttpClient封装的请求,则要传递文件之前,我们一 般会进行一次序列化,转化为二进制数组之类的,在网络上传输。这样的话,在Controller中的Action参数里,我们只需要接****这个二进制数组类 型的对象就可以了。但是如果客户端是Web Form呢,比如我们提交一个Form到指定的Controller的Action中,这个Action要接****什么类型的参数呢?或者我们问另外一个问题,如果我将Web Form提交到一个WebAPI的Action中 ,我要怎么去取出这个表单中的数据呢?其 实我们应该想到:我们的Action设置的参数之所以能够被赋值,是因为WebAPI的架构中在调用Action时将HTTP请求中的数据解析出来分别赋 值给Action中的参数,如果真是这样的话,我们只需要在Action中获取到HTTP请求,然后直接获取请求里面的数据,就能解决上面的问题。这 种想法是正确的,只不过,此时的HTTP请求已经不是最原始的HTTP Request,而是已经被转化成了HttpRequestMessage,在Action中,我们可以直接调用base.Requet来得到这个 HttpRequestMessage实例,通过这个实例我们就可以随心所欲的取出HTTP请求中想要的数据 T&->xe f=  
Dyh|F\T  
2.1从RequestMessage中获取普通表单数据 Mh=j^ [4Q  
这里的普通表单是指不包含File的表单,也就是说表单的enctype值不是multipart/form-data,这时,表单的数据默认情况下是以Json来传递的如下页面  6Ok]E`  
<form name="form" action="~/api/File/Register" method="post"> _*n 4W^8  
    <input type="text" name="userName" /> }r|$\ms  
    <br /> |)%;B%  
    <input type="text" name="passWord"  /> g1&q6wCg|  
    <br />     z)%]# QO  
    <input type="submit" value="Submit" /> "ed A  
</form> la>H&  
ApiContoller: 7Jn%c<s  
  public class FileController : ApiController EU.!/'<  
  { qb +Gjgp  
        [HttpPost] #O!gjZ,  
        public async Task  Register() `( _N9.>B  
        { y':65NMda  
              HttpContent content = Request.Content; , nW)A/?}  
             var jsonValue = await content.ReadAsStringAsync(); C'a#.LM  
            Console.WriteLine(jsonValue); af|x(:!H  
        } D/puK  
   } ["15~9  
执行:userName字段输入admin,passWord字段输入123456 I.kuYD62  
传递到服务器端输出的是:userName=admin&passWord=12345 uVa`2]NV r  
2.2从RequestMessage中获取multipart表单数据将view页面改写为: &[_D'jm+S0  
<form name="form" action="~/api/File/SubmitFile" method="post" enctype="multipart/form-data" > f*Yr*yC  
    <input type="text" name="userName2"  /> sU"sd7#A  
    <br /> l{hO"fzy  
    <input type="text" name="passWord2" /> EC7)M}H  
    <br /> D(#6H~QN%  
    <input type="file" name="file" id="upFile" /> K,dEa<p  
    <br /> h=dFSK?*D  
    <input type="submit" value="Submit" /> #aIV\G  
</form> 8JU{]Z!G<;  
ApiController代码: :]9CdkaU  
        public async Task<string> SubmitFile() dlBr2 9  
        { A5RM&y  
            // 检查是否是 multipart/form-data _Q7]Dw/w\  
            if (!Request.Content.IsMimeMultipartContent("form-data")) synueg  
                throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType); 7towjw r  
            var path = HttpContext.Current.Server.MapPath("~/File"); ~=?^v[T1  
            // 设置上传目录 fwv.^k x  
            var provider = new MultipartFormDataStreamProvider(path); NGmXF_kqN  
            // 接****数据,并保存文件 IgPU^?sp  
            var bodyparts = await Request.Content.ReadAsMultipartAsync(provider); 7E;`1lh7  
            var file = provider.FileData[0];//provider.FormData >f(M5v(D\  
            string orfilename = file.Headers.ContentDisposition.FileName.TrimStart('"').TrimEnd('"'); 'SKq<X%R;  
            FileInfo fileinfo = new FileInfo(file.LocalFileName);   {0 L)B{|  
            String ymd = DateTime.Now.ToString("yyyyMMdd", System.Globalization.DateTimeFormatInfo.InvariantInfo); L | #"Yn  
            String newFileName = DateTime.Now.ToString("yyyyMMddHHmmss_ffff", System.Globalization.DateTimeFormatInfo.InvariantInfo); 90gKGyxF  
            string fileExt = orfilename.Substring(orfilename.LastIndexOf('.')); U3t) yr h  
            fileinfo.CopyTo(Path.Combine(path, newFileName + fileExt), true); fK+[r1^  
            fileinfo.Delete(); fkD-mRKw  
            string result = ""; [#+klP$  
            // 获取表单数据 rmPJid[8B~  
            result += "formData userName: " + bodyparts.FormData["userName1"]; q0(-"}2l  
            result += "<br />"; `Db%:l^e  
            // 获取文件数据 Wx8n)  
            result += "fileData headers: " + bodyparts.FileData[0].Headers; // 上传文件相关的头信息 }U qL2KXi4  
            result += "<br />"; f!J?n]  
            result += "fileData localFileName: " + bodyparts.FileData[0].LocalFileName; // 文件在服务端的保存地址,需要的话自行 rename 或 move 5E}!TL$  
            return result; U%KsD 4B  
        } |v:fP;zc  
还有一种简单的方式 4v.i!U# {  
           public string Post() ,$"T/yYer  
        { bRI`ZT0  
                HttpPostedFile file = HttpContext.Current.Request.Files[0]; 9Rb tFwbn  
                string strPath = "D:\\MyProjects\\StudySolution\\RestDemo\\Upload\\test2.rar" ; @Op7OF Y%  
                file.S****eAs(strPath); 4_ kg/  
                string result = "0"; _cd=PZhI  
                return result; LNM#\fb  
          } /m h #o  
/<CgSW}  
注:上述的文件上传代码涉及到async、Task、await 这些关键字是 asp.net mvc 中异步编程的知识点,在这里暂不 J['i  
****过多解释,不了解的同学可以去先了解一下这块的内容,后期在我的系列主题文章中也还会有这块知识点的讲解,敬请关注! c? >;UzM  
解决第二个问题:客户端或者服务端的安全控制 %APeQy"6#^  
WebAPI的工作方式:HTTP的请求最先是被传递到HOST中的,如果WebAPI是被寄宿在IIS上的,这个HOST就是IIS上,HOST是没有能力也没有必 要进行请求的****的,请求通过HOST被转发给了HttPServer此时已经进入WebAPI的********范围,HttpServer是 System.Net.HTTP中的一个类,通过HttpServer,请求被封装成了WebAPI中的请求承载 类:HttpRequestMessage,这个封装后的请求可以经过一系列自定义的Handler来****,这些handler串联成一个 pipeline,最后请求会被传递给HttpControlDispather,这个类通过对路由表的检索来确定请求将被转发到的具体的 Controller中的Action。 Sb?HRoe_  
   由此我们早就可以看出,想要解决第二个问题,可以直接在Handler PipeLine中进行,这种AOP风格的过滤器(拦截器)在REST的Webservice的安全验证中应用很广,一般大家比较乐于在HTTP头或者在 HTTP请求的URL中加上身份验证字段进行身份验证,下面举一个在Http头中添加身份验证信息的小例子: AP0z~e  
3.1客户端客户端的customhandler用于将身份验证信息添加入报头 )u.%ycfeV  
class RequestCheckHandler : DelegatingHandler (~]0)J  
{ #W ely~  
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) GyGF<%nq  
        { vd0uI#g%#  
            request.Headers.Add("keyword", "ibeifeng"); U^&y*gX1  
            return base.SendAsync(request, cancellationToken); h{?cs%lZ  
        } e?fA3Fug  
} APqYf<W  
注:1.customhandler继承自DelegatingHandler类,上面已经说过,WebAPI的客户端和服务端被设计为相互对应的两套结构,所以不论是在客户端还是服务端,customhandler都是继承自DelegatingHandler类2.DelegatingHandler的sendAsync方法便是****请求和接受请求时会被调用的方法,该方法返回值是HttPResponseMessage,接****的值为HttpRequestMessage,符合我们的一般认知3.方法的最后,调用base.SendAsync是将Request继续向该pipeline的其他customHandler传递,并获取其返回值。由于该方法不包含Response的****逻辑,只需直接将上一个CustomHandler的返回值直接返回 W! )B%.Q  
=&y6mQ  
客户端主程序: $q0i=l&$&  
static void Main(string[] args)  0"koZd,c  
{ k<k@Tlo  
           HttpClient client = new HttpClient(new RequestCheckHandler() { InnerHandler = new HttpClientHandler() }); im"3n=  
            HttpResponseMessage response = client.GetAsync("http://localhost:47673/api/File/GetUserInfo?userName=admin").Result; 077 wk  
            response.Content.ReadAsStringAsync().ContinueWith((str) => { Console.WriteLine(str.Result); }); 7t|011<  
} 73kI%nNB  
客户端的主程序创建了一个HttpClient,HttpClient可以接受一个参数,该参数就是CustomHandler,此处我们嵌入了我们定义的 RequestUpHandler,用于对Request报头进行嵌入身份验证码的****,CustomHandler通过InnerHandler属性嵌 入其内置的下一个CustomHandler,此处,由于没有下一个CustomerHandler,我们直接嵌入HttpClientHandler用 于将HttpRequestMessage转化为HTTP 请求、将HTTP响应转化为HttpResponseMessage ; e)vk|  
M6z$*? <  
3.2服务端服务端的customHandler用于解析HTTP报头中的身份认证码 m-HBoN  
public class SecurityHandler : DelegatingHandler FXFyF*w2  
{ 2 t:CK  
   protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken G(MLq"R6U  
      cancellationToken) W|V 9:A  
    { }c ERCS\t  
            int matchHeaderCount = request.Headers.Count((item) => Aw=GvCo<  
            { P/T`q:<H   
                if ("keyword".Equals(item.Key)) Sv[$.^mb  
                { 5@`dKFB5  
                    foreach (var str in item.Value) *m:'~\[u  
                    { pu Z0_1uN  
                        if ("ibeifeng".Equals(str)) m " c6^)U  
                        { z Qx6r .  
                            return true; -E-e!  
                        } ;6{{hc4  
                    } jF|LPWl  
                } cD]#6PFA  
                return false; Ed>n/)Sm  
            }); Hzojv<c  
            if (matchHeaderCount>0) A\QrawBp0l  
            { ?xb2jZ/0X  
                return base.SendAsync(request, cancellationToken); lfyij[6q+  
            } xH{V.n&v  
            return Task.Factory.StartNew<HttpResponseMessage>(() => { return new HttpResponseMessage(HttpStatusCode.Forbidden); }); KBwY _  
   } z_A34@a  
} o;'-^ LJ  
另: oE$zOS&2  
FileController中加一个测试方法: tS6r4d%~=  
        [HttpGet] XseP[  
        public string GetUserInfo(string userName) 7WEoyd  
        { &ej |DM6  
            if (userName == "admin") ms/Q-  
            { >^Y)@ J  
                return "success"; wo_iCjmK  
            } p@%H. 5&&  
            else { .oz(,$CS"  
                return "failed"; `;z;=A*  
            }  4B'-tV  
y+P$}Nru  
        }注:代码的****逻辑很简单:如果身份验证码匹配成功,则通过base.SendAsync继续将请求向下传递,否则返回直接中断请求的传递,直接返回一个响应码为403的响应,指示没有权限。注意由于SendAsync的返回值需要封装在Task之中,所以需要使用Task.Factory.StartNew将返回值包含在Task中 !l~3K(&4  
将customHandler注入到HOST中本例中WebAPI HOST在IIS上,所以我们只需将我们定义的CustomHandler在Application_Start中定义即可 +M.!_2t$2  
        protected void Application_Start() -|`E'b81  
        { m,#Us  
            //省略其他逻辑代码 KAm$^N5  
            GlobalConfiguration.Configuration.MessageHandlers.Add(new SecurityHandler ()); BnIZ+fg=  
        } lv\^@9r  
由于WebAPI Host在IIS上,所以HttpServer和HttpControllerDispatcher不用我们手工**** 6|ENDd[  
在加上上面的****后,如果没有身份验证码的请求,会得到如下的响应 tk'3Q1L  
n^k Uu2g|  
eLyaTOZadu  
总结 L+TM3*a*  
1.使用WebAPI的目的 [C(>e0r  
  当你遇到以下这些情况的时候,就可以考虑使用Web API了。 19;F+%no#  
  a. 需要Web Service但是不需要SOAP j8{,u6w)-  
  b. 需要在已有的WCF服务基础上建立non-soap-based http服务 A.(xa+z?  
  c. 只想发布一些简单的Http服务,不想使用相对复杂的WCF配置 F=8gtk|U  
  d. 发布的服务可能会被带宽受限的设备访问 ,v>| Ub,  
  e. 希望使用****源框架,关键时候可以自己调试或者自定义一下框架 ]|)M /U *  
2.使用WebAPI的几种方式 与注意事项 VU(#5X%Pn  
3.使用WebAPI实现文件上传 KfMaVU=4P  
4.如何加强WebAPI的安全性 _:.'\d(  
%XK<[BF  
此部分内容涉及代码下载网盘: G~`nLC^Y  
链接: http://pan.baidu.com/s/1o6jZyiQ 4-{f$Z @  
密码:
本部分内容设定了隐藏,需要回复后才能看到
6yPh0n  
=%$BFg1a(  
bAm ,gP  
3 (R]QO`%'  
级别: VIP五级

UID: 465240
精华: 0
发帖: 36
威望: 36 点
学点: 28 点
贡献: 0 点
好评: 0 点
学币: 0 个
注册时间: 2014-05-16
最后登录: 2015-02-03
沙发(1楼)  发表于: 2014-06-16 19:23||

很好。值得学习~
级别: 终身会员

UID: 301401
精华: 0
发帖: 10
威望: 27 点
学点: 11 点
贡献: 0 点
好评: 0 点
学币: 0 个
注册时间: 2011-04-23
最后登录: 2014-09-27
板凳(2楼)  发表于: 2014-06-16 19:39||

很好,值得学习!
级别: VIP五级

UID: 209947
精华: 0
发帖: 60
威望: 492 点
学点: 46 点
贡献: 0 点
好评: 0 点
学币: 0 个
注册时间: 2009-12-29
最后登录: 2014-11-17
地板(3楼)  发表于: 2014-06-16 23:07||

速度来了风范股份两个管理规范了管理
我的青春我做主
级别: 终身会员

UID: 162806
精华: 0
发帖: 231
威望: 2355 点
学点: 2 点
贡献: 0 点
好评: 0 点
学币: 0 个
注册时间: 2009-05-22
最后登录: 2016-01-03
地下室(4楼)  发表于: 2014-06-17 05:08||

值得学习!
级别: 北风爱好者

UID: 469157
精华: 0
发帖: 2
威望: 2 点
学点: 4 点
贡献: 0 点
好评: 0 点
学币: 0 个
注册时间: 2014-06-17
最后登录: 2014-06-19
下水道(5楼)  发表于: 2014-06-17 13:02||

值得学习!
级别: 终身会员

UID: 301401
精华: 0
发帖: 10
威望: 27 点
学点: 11 点
贡献: 0 点
好评: 0 点
学币: 0 个
注册时间: 2011-04-23
最后登录: 2014-09-27
6楼  发表于: 2014-06-17 13:42||

很好!
级别: VIP五级

UID: 281964
精华: 0
发帖: 67
威望: 713 点
学点: 26 点
贡献: 0 点
好评: 0 点
学币: 0 个
注册时间: 2011-01-18
最后登录: 2015-05-06
7楼  发表于: 2014-06-17 17:41||

学习
级别: VIP五级

UID: 426179
精华: 0
发帖: 1
威望: 1 点
学点: 11 点
贡献: 0 点
好评: 0 点
学币: 0 个
注册时间: 2013-06-10
最后登录: 2016-09-26
8楼  发表于: 2014-06-21 23:13||

学习
级别: VIP三级

UID: 277461
精华: 0
发帖: 3
威望: 51 点
学点: 5 点
贡献: 0 点
好评: 0 点
学币: 0 个
注册时间: 2010-12-20
最后登录: 2014-06-22
9楼  发表于: 2014-06-22 11:08||

很好,值得学习!
上一主题下一主题
«12345»Pages: 1/14     Go