上一主题下一主题
«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)
%8L>|QOX  
tc|`cB3f  
Au=9<WB%H  
5z T~/6-(  
G*`H2-,  
.实例快速上手 -ASP.NET 4.5新特性WebAPI从入门到精通 ^xNs^wC.  
G\;a_]Q  
     在新出的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用于配置路由规则。 5n1aRA1  
环境准备 &*e(  
     建议使用VS2012以上版本创建WebAPI,如果是使用VS2010,需要安装VS2010 SP1升级包,MV****升级包,打****VS2012创建如下: #s}c K  
第一步:新建ASP.NET Web应用程序 `Z' h[-2`  
  [attachment=7091] 2_i9 q>I  
第二步:建议WebAPI 1 7..  
  [attachment=7092] W5f|#{&L:  
新生成的WebAPI项目和典型的MVC项目一样,包含主要的Models,Views,Controllers等文件夹和Global.asax文件 ,$,c<M  
  [attachment=7093] +n>_NVe  
注意:再次强调Views对于WebAPI来说没有太大的用途,Models中的Model主要用于保存Service和Client交互的对象,这些对象默认情况下会被转换为Json格式的数据进行传输,Controllers中的Controller对应于WebService来说是一个Resource,用于****服务。和普通的MVC一样,Global.asax用于配置路由规则(二)Models Ofm?`SE*|  
和WCF中的数据契约形成鲜明对比的是,MVC WebAPI中的Model就是简单的POCO,没有任何别的东西,如,你可以创建如下的Model j-CSf(qIj  
public class UserModel =.Hq]l6+  
{ {!/ha$(  
        public int Id { get; set; } Ed>Dhy6\r  
        public string UserName { get; set; } CD"D^\z  
        public string PassWord { get; set; } X9S` #N  
} [dXpz^Co  
   注意:Model必须****public的属性,用于json或xml反序列化时的赋值 H?a1XEY/  
(三)Controllers wr~Ydmsf  
MVC WebAPI中的Controllers和普通MVC的Controllers类似,不过不再继承于Controller,而改为继承API的ApiController,一个Controller可以包含多个Action,这些Action响应请求的方法与Global中配置的路由规则有关,在后面结束Global时统一说明 A#@9|3  
oLh 2:c  
(四)Global zQ=c6xvm8  
默认情况下,模板自带了两个路由规则,分别对应于WebAPI和普通MVC的Web请求,默认的WebAPI路由规则如下 ~ZuFMVR  
1            routes.MapHttpRoute( hh1 ?/  
2                 name: "DefaultApi", qrw"z iW  
3                 routeTemplate: "api/{controller}/{id}", !Y95e'f.x  
4                 defaults: new { id = RouteParameter.Optional } )zK6>-KWA  
5             ); ;J&p17~T9  
可以看到,默认路由使用的固定的api作为Uri的先导,按照微软官方的说法,用于区分普通Web请求和WebService的请求路径: u;/5@ADW  
可以看到,默认的路由规则只指向了Controller,没有指向具体的Action,因为默认情况下,对于Controller中的Action的匹配是和Action的方法名相关联的:具体来说,如果使用上面的路由规则,对应下面的Controller: zcrM3`Zh  
public class UserController : ApiController GLpl  
    { 1!P\x=Nn_  
        public  List<UserModel> allModeList = new List<UserModel>() { x,rK4L7U  
          new UserModel(){ Id=1,UserName="zhang", PassWord="123"}, .I#ss66h  
          new UserModel(){ Id=2,UserName="lishi", PassWord="123456"}, !*1Kjg3  
          new UserModel(){ Id=3,UserName="wang", PassWord="1234567"} f$|AU- |<  
        }; x Rp;y*  
        //Get  api/User/ '&+5L.  
        public IEnumerable<UserModel> GetAll() OciPd/6  
        { gbvMS*KQz  
            return allModeList; q,% lG$0v  
        } I8%Uy ap{  
        //Get api/User/1 N, SbJ Z  
        public IEnumerable<UserModel> GetOne(int id) { Py^fWQ5I~%  
            return allModeList.FindAll((m) => { return m.Id == id; }); WV?3DzeR  
        } 9,J^tN@^  
        //POST api/User/ 5oTj^W8M(  
        public bool PostNew(UserModel user) AQU^7O  
        { J`d_=C?J  
            try ^/M-*U8ab  
            { /73ANQ"  
                allModeList.Add(user); [~Vj(H=KwI  
                return true; EpUBO} q]  
            } HOSt0IHzty  
            catch { '?O_(%3F0  
                return false; /3KPK4!m  
            } ,~nrNkhp  
        } E'iN==p_:  
        //Delete api/User/ h#~\-j9>  
        public int DeleteAll() yR$ld.[uf  
        { #<o=W#[  
            return allModeList.RemoveAll((mode) => { return true; }); &G$K. q  
        } MJug no  
        //Delete api/User/1 y"T(Unvc  
        public int DeleteOne(int id) { ko+fJ &$  
            return allModeList.RemoveAll((m) => { return m.Id == id; }); 4d63+iM+}  
        } #D ]P3  
        //Put api/User =O amN7V=  
        public int PutOne(int id, UserModel user) *b,4qMr  
        { qtlcY8!  
            List<UserModel> upDataList = allModeList.FindAll((mode) => { return mode.Id == id; }); M97MIku~9  
            foreach (var mode in upDataList) m"f3hd4D_q  
            { g@|2z  
                mode.PassWord = user.PassWord; 1"S~#  
                mode.UserName = user.UserName; ^ ^T xx  
            } 7-VP)|L#G  
            return upDataList.Count; NnRX0]  
        } uJSzz:\  
} ~v6]6+   
   则,会有下面的对应关系: p8(Z{TSv  
   URL       HttpMethod   对应的Action名 !,9 ;AMO -  
   /api/User      GET        GetALL ;[B-!F>  
   /api/User/1    GET         GetOne x4XCR,-  
   /api/User      POST        PostNew q$'D}OHT  
   /api/User/1    DELETE     DeleteOne ~cul;bb#  
   /api/User      DELETE     DeleteALL +?v2MsF']  
   /api/User      PUT         PutOne I9L7,~s  
|f3 :9(p  
客户端JS调用 oVQbc \P3  
   function getAll() { #z =$*\u  
        $.ajax({ >e!Y63`  
            url: "api/User/", ^&KpvQNW_  
            type: 'GET', +!mEP >  
            success: function (data) { $]gflAe2  
                document.getElementById("modes").innerHTML = ""; IXpn(vX  
                $.each(data, function (key, val) { 5'/ff=  
                    var str = val.UserName + ': ' + val.PassWord; 2  ZyO  
                    $('<li/>', { html: str }).appendTo($('#modes')); nd }Z[)  
                }); D2I|Z  
            } OMAvJzK .  
        }).fail( + />f?+  
        function (xhr, textStatus, err) { }$b!/<7FD  
            alert('Error: ' + err); [lGxys)J  
        }); XTRF IY  
   } Ax*xa6_2  
function find() { JCci*F#r  
        $.ajax({ Z1_F)5pn  
            url: "api/User/1" , PMT}fg  
            type: 'GET', }Fsr"RER@{  
            success: function (data) { D!z'Y,.  
                document.getElementById("modes").innerHTML = ""; 1024L;  
                $.each(data, function (key, val) { Pn l}<i  
                    var str = val.UserName + ': ' + val.PassWord; biV NZdA  
                    $('<li/>', { html: str }).appendTo($('#modes')); 3taGb>15  
                });  ozKS<<  
            } W F:4p]0~)  
        }).fail( ]Ljb&*IEj  
        function (xhr, textStatus, err) { 8tVSa i8[  
            alert('Error: ' + err); <s%Ft  
        }); sOb]o[=  
    } j{D tjV8  
f8ZuG !U  
aKE`nA0\B  
    function add() { ydA@@C\&  
*Ms&WYN-  
        $.ajax({ zszmG^W{  
            url: "api/User/", +v$W$s&b-h  
            type: "POST", zl| XZ  
            dataType: "json", H_Xk;fM  
            data: { "Id":4,"UserName": "admin", "PassWord": "666666"}, @4MQ021(  
            success: function (data) { >; tE.CJH  
                getAll(); UWhJkJsX  
            } &n#yxv4  
        }).fail( o';/$xrH  
        function (xhr, textStatus, err) { z' Z[mrLq  
            alert('Error: ' + err); QPh3(K1w^  
        }); hgzNEx%^q  
AJE$Z0{q  
    } QleVW  
r ;MFVj{  
    function removeUser() { +S[3HX7H  
        $.ajax({ F_m' 9KX4E  
            url: "api/User/3", H5& ._  
            type: 'DELETE', ;<thEWH;Y  
            success: function (data) { +GL$[ 5G  
                document.getElementById("modes").innerHTML = ""; M_-L#FHX  
                getAll(); gu%i|-}  
            } "a}fwg9Y  
        }).fail( Lk P :l  
        function (xhr, textStatus, err) { rf%VSxD9  
            alert('Error: ' + err); $evuL3GY#  
        }); QxGcRlpLK  
    } )p1~Jx(\  
-zn_d]NV  
    function removeAll() { EApbaS}Up  
        $.ajax({ P9f`<o  
            url: "api/User/", -W<1BJE  
            type: 'DELETE', hjM?D`5x  
            success: function (data) { (_<,Oj#*S  
                document.getElementById("modes").innerHTML = ""; 5oSp/M  
                getAll(); YURMXbj  
            } 2 \}J*0  
        }).fail( h~](9e s  
        function (xhr, textStatus, err) { x.wDA3ys  
            alert('Error: ' + err); !khEep}  
        }); ^.\O)K {h  
    } \`xlD&F@U  
    function udpate() { 9A} *  
        $.ajax({ 3Cc#{X-+  
            url: "api/User/1", 3B 'j?+A  
            type: 'PUT', _"L6mcI6  
            dataType: "json", ~'KqiUY  
            data: { Id: 1, "UserName": "admin", "PassWord": "666666" }, /\h*v!:  
            success: function (data) { lV %1I@[M  
                document.getElementById("modes").innerHTML = ""; dCTyfXou[=  
                getAll(); U4 \v~n\  
            } !e~[U-  
        }).fail( = a60Xv  
        function (xhr, textStatus, err) { ix4]^  
            alert('Error: ' + err); 3cL iZ%6^  
        }); U].]K   
    } !Vw1w1  
这样就实现了最基本的CRUD操作。 ;VAyH('~  
扩展需求 %}VH5s9\  
问题1:我想按照用户名称(UserName)进行查询,怎么****? 5HV+7zU5  
****法:第一步:在UserController类中加一个方法名称叫:GetUserByName,如下所示: DJ:'<"zH7  
public UserModel GetUserByName(string userName) { m\0_1 #(  
            return allModeList.Find((m) => { return m.UserName.Equals(userName); }); 7 s5(eQI  
} qrlC U4  
第二步:在客户端index.cshtml中调用  |50sGJE(  
    function getUserByName() { Nb`qM]&  
        $.ajax({ Pa{  
            url: "api/User/zhang", y]fI7nu&  
            type: 'GET', Pp tuXq%U  
            success: function (data) { h&$h<zL[  
                document.getElementById("modes").innerHTML = ""; XAU%B-l:  
                var str = data.UserName + ': ' + data.PassWord; }Ze*/ p-  
                $('<li/>', { html: str }).appendTo($('#modes')); m"iA#3l*=  
            } hDW!pnj1  
        }).fail( aIV / c  
        function (xhr, textStatus, err) { 1^}I?PbqV  
            alert('Error: ' + err); 6`e7|ilh6  
        }); QkdcW>:a7  
   } <ukBAux,D  
如果URL是: url: "api/User/zhang",将会报错:Bad Request w#|L8VAh  
原因是他会自动调用我们的GetOne(int id) 这个方法,类型转换出错 + O.-o/  
解决****法: ){5Nod{}a  
改变URL为: url: "api/User/?userName=zhang", AAevN3a#nI  
问题2:我想按用户名称(UserName) 和用户密码(PassWord)一起来进行查询,怎么****? >CqzC8JF  
解决****法 27e!KG[&  
第一步:UserController类中,可以重载一个GetUserByName的方法,如下所示: E+]9!fDy<  
    public UserModel GetUserByName(string userName) { J +u}uN@   
            return allModeList.Find((m) => { return m.UserName.Equals(userName); }); cQU;PH]  
        } hw^&{x  
第二步:客户端调用: f<Hi=Qpm  
function getUserByName() { o8bdL<  
        $.ajax({ t;e&[eg  
            url: "api/User/?userName=zhang&passWord=123", //这里尤其需要注意 mLX/xM/T?/  
            type: 'GET', l#'V SFm&  
            success: function (data) { A9$x8x*Lt  
                document.getElementById("modes").innerHTML = ""; }Y~Dk]*  
                var str = data.UserName + ': ' + data.PassWord; ky2 bj}"p9  
                $('<li/>', { html: str }).appendTo($('#modes')); *; o%*:  
            } H@Z_P p?  
        }).fail( 6_Fr\H  
        function (xhr, textStatus, err) { 6>a6;[  
            alert('Error: ' + err); <+roY"  
        }); Zse&{  
   } `^bP9X_a  
B*(]T|ff<  
路由规则扩展 B>,e HXW  
和普通的MVC一样,MVC WebAPI支持自定义的路由规则,如:在上面的操作中,路由规则使用 7n8nJTU{4j  
"api/{controller}/{id}" i9;  
则限定了使用GET方式利用URL来传值时,controller后面的接****参数名为id,但是在Controller中,如果GetOne方法的接****参数名为key,是不会被匹配的,这是只需要新增一个新的路由规则,或修改原先的路由规则为: Nd~B$venh  
"api/{controller}/{key}",如下所示: j / 5  
config.Routes.MapHttpRoute( ig,.>'+l  
                name: "DefaultApi", j#l= %H  
                routeTemplate: "api/{controller}/{key}", X?5{2ulrI  
                defaults: new { key = RouteParameter.Optional } 9`tK 9  
            ); NJUKH1lIhR  
当然,可以对路由进行更深的扩展,如:扩展成和普通MVC一样的路由: o?`FjZ6;x  
"api/{controller}/{action}/{id}" *_(X$qfoW  
这样,就要求同时使用Action和HTTP方法进行匹配当然,根据微软的说法,这种使用是不被****的,因为这不符合大家对WebService的一般认知: 4Uy%wB  
使用Attribute声明HTTP方法 a3@E`Z  
       [HttpGet] 2|nm> 4  
       public IEnumerable<TestUseMode> FindAll() HK;NR.D  
       [HttpGet] GEc6;uz<  
       public IEnumerable<TestUseMode> FindByKey(string key) >Bt82ibN  
       [HttpPost] YvR bM  
       public bool Add(TestUseMode mode) =RoG?gd{R  
       [HttpDelete] ci0A!wWD  
       public int RemoveByKey(string key) VD;*UkapZx  
       [HttpDelete] c_J9C Kqc  
       public int RemoveAll() /9`4f"  
       [HttpPut] n;g'?z=hy  
       public int UpdateByKey(string key, string value) T5XXC1+  
       [NonAction]   JJ: ku&Mb  
       public string GetPrivateData() &f*o rM:  
当然,我只列出了方法名,而不是这些方法真的没有方法体...方法体是不变的,NoAction表示这个方法是不接****请求的,即使以GET****头。如果感觉常规的GET,POST,DELETE,PUT不够用,还可以使用AcceptVerbs的方式来声明HTTP方法,如: {,>G 1>Yv  
[AcceptVerbs("MKCOL", "HEAD")] fvu{(Tb  
public int UpdateByKey(string key, string value) =sJ?]U  
{ =[B\50]  
            List<TestUseMode> upDataList = allModeList.FindAll((mode) => { if (mode.ModeKey == key) return true; return false; }); ]aX@(3G1s  
            foreach(var mode in upDataList) p3s i\Fm!  
            { K~W(ZmB  
                mode.ModeValue = value; s9 - qR_  
            } )Z7Vm2a  
            return upDataList.Count; O]N/(pe:d  
} -3Kh >b)  
附:什么是REST风格? 参考:什么是REST风格 `7 "="T~ *  
http://hi.baidu.com/yankaiwei/item/1f0b37dd922d53ef3cc2cb69 Knwy%5.Z  
第二部分:综合示例:应用ASP.NET MV****+WebAPI+FluentData****发Web应用 Q,U0xGGz  
第一步:创建数据库 9+ 'i(q z  
NorthWind数据库的Customers表 {j2V k)\[i  
Create DataBase NorthWind XKp&GE@Y  
Go 'l*X?ccKy  
Use NorthWind ImXYI7PL  
Go )[rVg/m  
CREATE TABLE [dbo].[Customers]( L2H  
    [CustomerID] [nchar](5) NOT NULL, jJ$\WUQ.  
    [CompanyName] [nvarchar](40) NOT NULL, m:@y_:X0  
    [ContactName] [nvarchar](30) NULL, +kSu{Tc  
    [ContactTitle] [nvarchar](30) NULL, $n<a`PdH  
    [Address] [nvarchar](60) NULL, "\4W])30  
    [City] [nvarchar](15) NULL, ;1k& }v&  
    [Region] [nvarchar](15) NULL, EU[\D;  
    [PostalCode] [nvarchar](10) NULL, 6bm7^e(  
    [Country] [nvarchar](15) NULL, 4@9Pd &I  
    [Phone] [nvarchar](24) NULL, l=DF)#>w  
    [Fax] [nvarchar](24) NULL, ]$smFF  
CONSTRAINT [PK_Customers] PRIMARY KEY CLUSTERED ?I+L  
( JR!-1tnc  
    [CustomerID] ASC v,w af`)J  
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] M(X _I`\E  
) ON [PRIMARY] >Hf {Mx{<  
s%)f<3=a  
GO *yBVZD|?H  
第二步:创建 FluentData.Entity层,创建Customer实体类 sT^R0Q'>  
namespace FluentData.Entity u)oAQ< w  
{ OZ&/&?!XE  
    public class Customer <>HtXn/  
    { K(;qd Ir  
        public string CustomerID { get; set; } JL M Xkcc  
        public string CompanyName { get; set; } s= %3`3Fo  
        public string ContactName { get; set; } "OLg2O^  
        public string ContactTitle { get; set; } ^KhJBM/Z  
        public string Address { get; set; } Wga2).j6  
        public string City { get; set; } HE&)N clY  
        public string Region { get; set; } QAkK5,`vV.  
        public string PostalCode { get; set; } >7W)iwF  
        public string Country { get; set; } IxbQ6  
        public string Phone { get; set; } $8\u  
        public string Fax { get; set; } tVG;A&\,6  
    } ;@Zuet  
} i:s=  
第三步:利用FluentData****数据的持久化 Im6gWDdq@6  
首先引入FluentData.cs (见附件) k8?._1t  
其次:创建DBHelper类,代码如下: Ta\F~$M  
public class DBHelper JC cYFtW  
    { k A3K   
        public static IDbContext Context() { eha|cAq  
            //return new DbContext().ConnectionString("server=127.0.0.1;uid=sa;pwd=sa;database=TestDB", new SqlServerProvider()); F!z ^0+H(  
            return new DbContext().ConnectionStringName("connString", new SqlServerProvider()); ]N 9N][n  
        } I8XP`Ccq  
   } `,}7LfY  
然后不要忘记修改ASP.NET MVC层所在的Web.config,加入数据库连结字符串: +zh\W9  
  <connectionStrings> lp(2"$nQ  
    <add name="connString" connectionString="server=127.0.0.1;database=Northwind;uid=sa;pwd=sa;"/> ,8r?C!m]  
  </connectionStrings> ].Yz =:  
第三步:创建 CustomerService数据持久化类,代码如下: e|oMbTZ5m  
public class CustomerService SnG(/1C8  
    { xf&[QG+Ef  
        private IDbContext context = DBHelper.Context(); _0+X32HjJ  
        public Customer Select(string customerId){ wQG?)aaM  
*47/BLys<  
            return context.Select<Customer>("*").From("Customers").Where("CustomerID=@0").Parameters(customerId) i.^ytbH  
                   .QuerySingle(); u)<]Pb})r  
             opH!sa@U  
        } $z[S0Cm  
        public List<Customer> SelectAll() { :(]fC~G~  
            return context.Select<Customer>("*").From("Customers").QueryMany(); Gko"iO#  
        } 0!,uo\`  
P<IDb%W  
        public List<Customer> SelectAll(string sortExpression) { "}v.>L<P  
            if (String.IsNullOrEmpty(sortExpression)) return null; eyK xnBz  
            return context.Select<Customer>("*").From("Customers").OrderBy(sortExpression).QueryMany(); R%KF/1;/  
        } t8-P'3,Q$  
qt}M&=}8Q  
        public List<Customer> SelectAll(int currentPageIndex,int maxRows, string sortExpression) p=:Vpg<!  
        { J'Pyn  
            var select = context.Select<Customer>("*").From("Customers"); 8GP17j  
            if (maxRows > 0) { SO6)FiPy!n  
                if (currentPageIndex == 0) currentPageIndex = 1; dJgLS^1E  
                select.Paging(currentPageIndex, maxRows); _e@8E6#ce  
            } *:yG)J 3F  
            if (!string.IsNullOrEmpty(sortExpression)) { %]Z4b;W[Y  
                select.OrderBy(sortExpression); kY$EK]s  
            } 74M9z  
gbuh04#~  
            return select.QueryMany();  ;v.[aq  
        } j4owo#OB-  
ZHK>0>;  
        public int CountAll() { `sKyvPtG  
            return context.Sql("select count(*) from Customers").QuerySingle<int>();  )bF l-  
        } M<Wi:r:  
OL 0YjU@  
        public int Insert(Customer customer) { 6&x\!+]F8  
            return context.Insert<Customer>("Customers", customer).Execute(); \!\:p/f  
        } 1w=.vj<d8  
A5Hx $.Z  
        public int Update(Customer customer) { ;}+M2Ec51  
            return context.Update<Customer>("Customers", customer).Where("CustomerID", customer.CustomerID).Execute(); D 5rH6*J  
        } .u)KP*_  
kD#n/R Bgf  
        public int Delete(string customerId) { } [#8>T  
            return context.Delete("Customers").Where("CustomerID", customerId).Execute(); iL;V5|(sb  
        } Ve(<s  
5$ =[x!x  
        public int Delete(Customer customer) ^^v3iCT  
        { @m5J%8>k  
            return this.Delete(customer.CustomerID); hA ){>B<;  
        } ^(TCUY~f&  
    } PaSwfjOnqr  
第四步:Web API,创建CustomerController / <(|4e  
注意要引用:FluentData.Entity及FluentData.DAL 程序集 tCrEcjT-  
public class CustomerController : ApiController |6Y:W$7k  
    { (n kg  
        private CustomerService customerService = new CustomerService(); 1R}9k)JQ  
        //Select All >I S4  
        public IEnumerable<Customer> Get() !ldEy#"X  
        { Ou1kSG|kM  
            return customerService.SelectAll(); uM$b/3%s  
        } g ba1R  
 z/91v#}.  
        //Select By Id :WWHEZK  
        public Customer Get(string id) ,IhQ%)l  
        { lai@,_<GV  
            return customerService.Select(id); ~EmK ;[Z  
        } 6/cm TT$i  
P6%qNR/ x  
        //Insert A@~9r9Uf  
        public void Post(Customer customer) b6S"&hs  
        { >0SG]er@  
            customerService.Insert(customer); \ 3E%6L  
        } c*#$sZ@YA  
c k[uvH   
b6;MTz*k>  
        //Update R}(Rv3>Xx  
        public void Put(string id, Customer obj) ipu~T)}  
        { Eva&FHRTY  
            customerService.Update(obj); 6-$95. Y2  
        } }cUO+)!Y  
.HTRvE`X  
        //Delete VSO(DCr"L  
        public void Delete(string id) "w 4^i!\  
        { l^tRy_T:-  
            customerService.Delete(id); .,VLQ btg  
        } L8E4|F}  
   } k N*I_#  
第五步:View层代码 2ETv H~23  
uQ ]ZMc  
namespace MyWebApI.Controllers ,y0 &E8Z  
{ "\lO Op^-  
    public class HomeController : Controller Nj?Q{ztS  
    { s{c|J#s  
        public ActionResult Index() ,<7 HLV  
        { `"V}Wq ?I  
            return View(); Fv(FRZ)  
        } !gsrPM  
u!HbS*jqq  
        public ActionResult Test() { 7^!iGhI]r  
            return View(); 3k8nWT:wT  
        } av'[k<  
=~ ,2E;#X  
        public ActionResult CustomerManager() { ?hrz@k|  
            return View(); gLFSZ  
        } ([SJ6ff]&  
    } @tRDKPh  
} IW}Wt{'m  
然后创建View V<}chLd,  
I4 <_y5  
    <table id="customerTable" border="1" cellpadding="3"  style="width:700px"> wSZMHIW  
        <tr> oOLj? 0t  
            <th>Customer ID</th> Vvv;m5.  
            <th>Company Name</th> WX f[W  
            <th>Contact Name</th> "2tKh!?Q  
            <th>Country</th> zlkW-rRkR  
            <th>Actions</th> &- My[t  
        </tr> s gZlk9x!Q  
        <tr> pz{ ]O_px  
            <td><input type="text" id="txtCustomerId"  style="width:100px" size="5"/></td> z 63y8  
            <td><input type="text" id="txtCompanyName"  style="width:150px" /></td> m*lcIa  
            <td><input type="text" id="txtContactName"  style="width:150px"/></td> On[yL$?  
            <td><input type="text" id="txtCountry"  style="width:150px"/></td> (*T$:/zI S  
            <td><input type="button" name="btnInsert" value="Insert"  style="width:150px"/></td> fgA-+y  
        </tr> vr6YE;Rs  
    </table> * ]D{[hV  
d+ [2Sm(7  
<script type="text/j****ascript"> R$Qhu xT|  
    $(function () { iG=Di)O  
        $.getJSON("api/Customer", LoadCustomers); v5{2hCdt  
    }); *q[;-E(fZ#  
)4 ,U  
    function LoadCustomers(data) { 5-&"nn2*}1  
        $("#customerTable").find("tr:gt(1)").remove(); Qte%<POx+  
        $.each(data, function (key, val) { )NqRu+j  
            var tableRow = '<tr>' + ID{XZ  
                            '<td>' + val.CustomerID + '</td>' + kN#3HI]8  
                            '<td><input type="text" value="' + val.CompanyName + '" /></td>' + cVr+Wp7K#|  
                            '<td><input type="text" value="' + val.ContactName + '" /></td>' + DhsvN&yNM  
                            '<td><input type="text" value="' + val.Country + '" /></td>' + 8 tIy"5  
                            '<td><input type="button" name="btnUpdate" value="修改" /> <input type="button" name="btnDelete" value="****" /></td>' + vF[ 4kDHk  
                            '</tr>'; d..JW{  
            $('#customerTable').append(tableRow); q)AX*T+  
        }); cF?0=un  
H4sc7-  
        $("input[name='btnInsert']").click(OnInsert); :l?mNm5  
        $("input[name='btnUpdate']").click(OnUpdate); 64>CfU(  
        $("input[name='btnDelete']").click(OnDelete); k*Aee7  
    } ~03MH'  
t% <y^Wa=  
    function OnInsert(evt) { -*Th=B-  
        var customerId = $("#txtCustomerId").val(); %&q}5Y4!  
        var companyName = $("#txtCompanyName").val(); Z_ Y'#5o#  
        var contactName = $("#txtContactName").val(); }d;6.~Gw  
        var country = $("#txtCountry").val(); Fmz+ Xb  
        var data = '{"CustomerID":"' + customerId + '","CompanyName":"' + companyName + '","ContactName":"' + contactName + '","Country":"' + country + '"}'; (Nv -wU  
9.il1mAKg  
        $.ajax({ %/5Wj_|p  
            type: 'POST', N]6t)Zv  
            url: '/api/Customer/', r9L--#=z  
            data: data, l=(( >^i  
            contentType: "application/json; charset=utf-8", ^H{YLO  
            dataType: 'json', Dg{d^>T!_x  
            success: function (results) { aq l8Or1[  
                $("#txtCustomerId").val(''); _{gqi$Mi  
                $("#txtCompanyName").val(''); BenyA:W"  
                $("#txtContactName").val(''); v\@ RwtP  
                $("#txtCountry").val(''); ,]W|"NUI  
                $.getJSON("api/customers" + new Date().getTime(), LoadCustomers); KB(W'M_D\  
                alert('添加成功!'); W I MBw mg  
            } 1 <+ aF,  
        }).fail( +@?'dw  
        function (xhr, textStatus, err) { lG%697P  
            alert('添加失败,原因如下: ' + err); $~W5! m  
        }); ;l ZKgi8`  
    } lv'WRS'}  
    function OnUpdate(evt) { %A=/(%T>  
,w H~.LHi  
        var input; t"q'"FX  
        var customerId = $(this).parent().parent().children().get(0).innerHTML; a%`%("g!  
| |awNSt  
        input = $($(this).parent().parent().children().get(1)).find("input"); e5P9P%1w  
        //input.removeAttr("disabled"); , f$P[c  
        var companyName = input.val(); orH6R8P]  
~uty<fP  
        input = $($(this).parent().parent().children().get(2)).find("input"); h-?yed*?  
        //input.removeAttr("disabled"); jS##zC  
        var contactName = input.val(); kaB|+U9^  
H~:oW~Ah  
        input = $($(this).parent().parent().children().get(3)).find("input"); /}8Au$nA  
        //input.removeAttr("disabled"); C|TQf8  
        var country = input.val(); ! ZH "$m|  
&l1t5 !  
        var data = '{"CustomerID":"' + customerId + '","CompanyName":"' + companyName + '","ContactName":"' + contactName + '","Country":"' + country + '"}'; ;'8P/a$  
         RBn/7  
        $.ajax({ ZZI} Ot{  
            type: 'PUT', }%/mPbd#  
            url: '/api/Customer/' + customerId, z}u`45W+  
            data: data, 6dr 'nP  
            contentType: "application/json; charset=utf-8", >,]a>V  
            dataType: 'json', SefhOh^,V  
            success: function (results) { &'W7-Z\j-  
                $.getJSON("api/Customer" + new Date().getTime(), LoadCustomers); o OpEpQ}}q  
                alert('修改成功 !'); )2?]c  
            } |nx3x   
        }).fail( =f!A o:Uc  
        function (xhr, textStatus, err) { [#uhMn^  
            alert('修改失败,原因如下: ' + err); =|1_ 6.tz  
        }); `~)?OTzU#  
    } pJ/]\>#5  
    function OnDelete(evt) { oLKliA=q  
        var customerId = $(this).parent().parent().children().get(0).innerHTML; Tty'ysH  
        //var data = '{"id":"' + customerId + '"}'; 5WHz_'c  
        //var row = $(this).parent().parent(); + m-88  
_F6<ba}o3  
        $.ajax({ #U`AK9rP_g  
            type: 'DELETE', :VlA2Ih&q  
            url: '/api/Customer/' + customerId, YXWDbr:JX  
            contentType: "application/json; charset=utf-8", 2.%)OC!q&5  
            dataType: 'json', i;^lh]u  
            success: function (results) { hO8xH +;  
                $.getJSON("api/Customer?"+new Date().getTime(), LoadCustomers); $8eiifj  
                alert('成功****!'); ; oa+Z:;f  
            } ux TgK'3  
        }).fail( rah"\f2  
        function (xhr, textStatus, err) { . |[{$&B  
            alert('****失败,原因如下: ' + err); JGHj(0j  
        }); S~aWun  
_ZhQY,  
    } #8et91qw  
</script> *zPqXtw!j  
第三部分:Web API高级部分 sF!#*Y  
在第一部分和大家一起学习了建立基本的WebAPI应用,第二部分写了一个综合示例,立刻就有人想到了一些问题:1.客户端和WebService/WebAPI之间文件传输2.客户端或者服务端的安全控制要解决这些问题,要了解一下WebAPI的基本工作方式。 $XQg at@&]  
(一)WebAPI中工作的Class _|M8xI  
在MVC中大家都知道,获取Request和Response使用HttpRequest和HttpResponse两个类,在WebAPI中使用两外两个类:HttpRequestMessage 和HttpResponseMessage,分别用于封装Requset和Response。除了这两个类之外,还有一个常见的抽象 类:HttpMessageHandler,用于过滤和****HttpRequestMessage和HttpResponseMessage 0}<blU  
`(HD'fud3  
(二)解决第一个问题:客户端和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请求中想要的数据 0C :8X   
P  y v>  
2.1从RequestMessage中获取普通表单数据 _" 9 q(1  
这里的普通表单是指不包含File的表单,也就是说表单的enctype值不是multipart/form-data,这时,表单的数据默认情况下是以Json来传递的如下页面 nUj`#%  
<form name="form" action="~/api/File/Register" method="post"> {FFdMdxy-  
    <input type="text" name="userName" /> i24k ]F  
    <br /> ExtC\(X;  
    <input type="text" name="passWord"  /> 9 ~W]D!m,  
    <br />     4x)vy -y  
    <input type="submit" value="Submit" /> , ]1f)>  
</form> m)L50ot:/  
ApiContoller: d?X,od6  
  public class FileController : ApiController hdw.S`~}%  
  { >ZkL`!:s  
        [HttpPost] qj~=qV0p  
        public async Task  Register() o_Zs0/  
        { FcM)v"bF&]  
              HttpContent content = Request.Content; !nl-}P,  
             var jsonValue = await content.ReadAsStringAsync(); Dc0=gq0  
            Console.WriteLine(jsonValue); ]|!|3lQ  
        } z"mVE T  
   } ;l!<A  
执行:userName字段输入admin,passWord字段输入123456 &V*MNi,4Z  
传递到服务器端输出的是:userName=admin&passWord=12345 S'HA ]  
2.2从RequestMessage中获取multipart表单数据将view页面改写为: ZX&e,X~V  
<form name="form" action="~/api/File/SubmitFile" method="post" enctype="multipart/form-data" > w? _8OJ  
    <input type="text" name="userName2"  /> /g>-s&w  
    <br /> _fk}d[q0  
    <input type="text" name="passWord2" /> E\D,=|Mul  
    <br /> 1,=:an  
    <input type="file" name="file" id="upFile" /> wzy[sB274  
    <br /> 7NRa&W2  
    <input type="submit" value="Submit" /> #EUT"^:d  
</form> yFk|8d-|  
ApiController代码: v CsE|e MP  
        public async Task<string> SubmitFile() fSd|6iFH  
        { \\ItN  
            // 检查是否是 multipart/form-data < xeB9  
            if (!Request.Content.IsMimeMultipartContent("form-data")) fLs>|Rh  
                throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType); EE]xZz>o  
            var path = HttpContext.Current.Server.MapPath("~/File"); w!<e#Z]3b  
            // 设置上传目录 4~1b  
            var provider = new MultipartFormDataStreamProvider(path); xkFa  
            // 接****数据,并保存文件 T"htWo{v>  
            var bodyparts = await Request.Content.ReadAsMultipartAsync(provider); ZFMO;'m&  
            var file = provider.FileData[0];//provider.FormData 8Kl&_-l{b  
            string orfilename = file.Headers.ContentDisposition.FileName.TrimStart('"').TrimEnd('"'); K?,? .!ev  
            FileInfo fileinfo = new FileInfo(file.LocalFileName);   }ACg#;>/+  
            String ymd = DateTime.Now.ToString("yyyyMMdd", System.Globalization.DateTimeFormatInfo.InvariantInfo); }.D18bE(  
            String newFileName = DateTime.Now.ToString("yyyyMMddHHmmss_ffff", System.Globalization.DateTimeFormatInfo.InvariantInfo); [@/p 8I  
            string fileExt = orfilename.Substring(orfilename.LastIndexOf('.')); ;Egl8Vhr  
            fileinfo.CopyTo(Path.Combine(path, newFileName + fileExt), true); xKBi".wA  
            fileinfo.Delete(); x(sKkm`Q  
            string result = ""; 0= bXL!]  
            // 获取表单数据 }>AA[ba"'  
            result += "formData userName: " + bodyparts.FormData["userName1"]; 8h)7K/!\  
            result += "<br />"; c/.s`hz  
            // 获取文件数据 4 /{pz$  
            result += "fileData headers: " + bodyparts.FileData[0].Headers; // 上传文件相关的头信息 dY. X/f  
            result += "<br />"; bbS,pid1  
            result += "fileData localFileName: " + bodyparts.FileData[0].LocalFileName; // 文件在服务端的保存地址,需要的话自行 rename 或 move  7Oe$Ou  
            return result; VDv.N@ ) 7  
        } OR+_s @Yg  
还有一种简单的方式 Ha]vG@?+  
           public string Post() xc-[gt6  
        { |Wck-+}U  
                HttpPostedFile file = HttpContext.Current.Request.Files[0]; (bsywM  
                string strPath = "D:\\MyProjects\\StudySolution\\RestDemo\\Upload\\test2.rar" ; 1Hhr6T^)  
                file.S****eAs(strPath); V/DMkO#a  
                string result = "0"; 0FL'8!e<  
                return result; WoN JF6=?  
          } "fv+}'  
E0HE@pqr  
注:上述的文件上传代码涉及到async、Task、await 这些关键字是 asp.net mvc 中异步编程的知识点,在这里暂不 3rZPVR$))  
****过多解释,不了解的同学可以去先了解一下这块的内容,后期在我的系列主题文章中也还会有这块知识点的讲解,敬请关注! Nf* .r  
解决第二个问题:客户端或者服务端的安全控制 ^wSGrV'  
WebAPI的工作方式:HTTP的请求最先是被传递到HOST中的,如果WebAPI是被寄宿在IIS上的,这个HOST就是IIS上,HOST是没有能力也没有必 要进行请求的****的,请求通过HOST被转发给了HttPServer此时已经进入WebAPI的********范围,HttpServer是 System.Net.HTTP中的一个类,通过HttpServer,请求被封装成了WebAPI中的请求承载 类:HttpRequestMessage,这个封装后的请求可以经过一系列自定义的Handler来****,这些handler串联成一个 pipeline,最后请求会被传递给HttpControlDispather,这个类通过对路由表的检索来确定请求将被转发到的具体的 Controller中的Action。 !]7b31$M_  
   由此我们早就可以看出,想要解决第二个问题,可以直接在Handler PipeLine中进行,这种AOP风格的过滤器(拦截器)在REST的Webservice的安全验证中应用很广,一般大家比较乐于在HTTP头或者在 HTTP请求的URL中加上身份验证字段进行身份验证,下面举一个在Http头中添加身份验证信息的小例子: d j9i*#F  
3.1客户端客户端的customhandler用于将身份验证信息添加入报头 OhaoLmA}6  
class RequestCheckHandler : DelegatingHandler I)jAdd  
{ bhg6p$411  
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) o*d(;  
        { Q;y4yJ$wI  
            request.Headers.Add("keyword", "ibeifeng"); )BaGY  
            return base.SendAsync(request, cancellationToken); Iq19IbR8  
        } H,EZ% Gl  
} (}T},ygQ  
注:1.customhandler继承自DelegatingHandler类,上面已经说过,WebAPI的客户端和服务端被设计为相互对应的两套结构,所以不论是在客户端还是服务端,customhandler都是继承自DelegatingHandler类2.DelegatingHandler的sendAsync方法便是****请求和接受请求时会被调用的方法,该方法返回值是HttPResponseMessage,接****的值为HttpRequestMessage,符合我们的一般认知3.方法的最后,调用base.SendAsync是将Request继续向该pipeline的其他customHandler传递,并获取其返回值。由于该方法不包含Response的****逻辑,只需直接将上一个CustomHandler的返回值直接返回 XmVst*2=  
dF! B5(  
客户端主程序: xf8e"mD  
static void Main(string[] args) wUg=j nY   
{ M $EHx[*5  
           HttpClient client = new HttpClient(new RequestCheckHandler() { InnerHandler = new HttpClientHandler() }); vz:VegS  
            HttpResponseMessage response = client.GetAsync("http://localhost:47673/api/File/GetUserInfo?userName=admin").Result; H8@z/  
            response.Content.ReadAsStringAsync().ContinueWith((str) => { Console.WriteLine(str.Result); }); 4 8; b  
} _\@zq*E  
客户端的主程序创建了一个HttpClient,HttpClient可以接受一个参数,该参数就是CustomHandler,此处我们嵌入了我们定义的 RequestUpHandler,用于对Request报头进行嵌入身份验证码的****,CustomHandler通过InnerHandler属性嵌 入其内置的下一个CustomHandler,此处,由于没有下一个CustomerHandler,我们直接嵌入HttpClientHandler用 于将HttpRequestMessage转化为HTTP 请求、将HTTP响应转化为HttpResponseMessage qM Qu!%o  
SuV3$-);z  
3.2服务端服务端的customHandler用于解析HTTP报头中的身份认证码 *cX i*7|=  
public class SecurityHandler : DelegatingHandler Y)GU{  
{ BP&] t1p  
   protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken ofYZ! -V  
      cancellationToken) L&|^y8  
    { WkpHe  
            int matchHeaderCount = request.Headers.Count((item) => u?z,Vs"  
            { ^H&`e"|R9  
                if ("keyword".Equals(item.Key)) SFEDR?s   
                { x>J(3I5_b  
                    foreach (var str in item.Value) NOAz"m+o  
                    { elD|b=(-  
                        if ("ibeifeng".Equals(str)) pgv, Su  
                        { :yw(Co]f  
                            return true; k $# ,^)T  
                        } ;c@B+RquR  
                    } !"F8jA}  
                } *u[@C  
                return false; }4,[oD  
            }); B" ]a8}u  
            if (matchHeaderCount>0) -2C^M> HZ  
            { :$XlYJrjK  
                return base.SendAsync(request, cancellationToken); |$GPJaNqa  
            } U4iVI#f  
            return Task.Factory.StartNew<HttpResponseMessage>(() => { return new HttpResponseMessage(HttpStatusCode.Forbidden); }); P|;v>  
   } 9'vf2) "  
} #~r+Z[(,p  
另: ,o3`O|PiK  
FileController中加一个测试方法: M pz9}[`3g  
        [HttpGet] Ub wmn!~  
        public string GetUserInfo(string userName) 90=gP  
        { c11;(  
            if (userName == "admin") & }_tALg  
            { cNy*< Tv  
                return "success"; D3+<16[,  
            } rF/<}ye/4M  
            else { @G|z _  
                return "failed"; T9>,Mx%D[  
            } "2"2qZ*h}  
:?)q"hE  
        }注:代码的****逻辑很简单:如果身份验证码匹配成功,则通过base.SendAsync继续将请求向下传递,否则返回直接中断请求的传递,直接返回一个响应码为403的响应,指示没有权限。注意由于SendAsync的返回值需要封装在Task之中,所以需要使用Task.Factory.StartNew将返回值包含在Task中 0.U- tg0  
将customHandler注入到HOST中本例中WebAPI HOST在IIS上,所以我们只需将我们定义的CustomHandler在Application_Start中定义即可 Bv 7os3xb  
        protected void Application_Start() C&.Q|S2_  
        { H00iy$R  
            //省略其他逻辑代码 @Fb 2c0?Y  
            GlobalConfiguration.Configuration.MessageHandlers.Add(new SecurityHandler ()); i\ )$  
        } lQ2vQz-J  
由于WebAPI Host在IIS上,所以HttpServer和HttpControllerDispatcher不用我们手工**** U@LIw6B!KL  
在加上上面的****后,如果没有身份验证码的请求,会得到如下的响应 7K24sHw;%  
oe=W}y_k  
22EI`}"J  
总结 ]OC?g2&6  
1.使用WebAPI的目的 6'.CW4L  
  当你遇到以下这些情况的时候,就可以考虑使用Web API了。 0)9n${P7d  
  a. 需要Web Service但是不需要SOAP ]QF*\2b-I2  
  b. 需要在已有的WCF服务基础上建立non-soap-based http服务 *nHkK!d<N  
  c. 只想发布一些简单的Http服务,不想使用相对复杂的WCF配置 M"ZeK4qh  
  d. 发布的服务可能会被带宽受限的设备访问 uAV-wc  
  e. 希望使用****源框架,关键时候可以自己调试或者自定义一下框架 pH396GFIW  
2.使用WebAPI的几种方式 与注意事项 "iuNYM5 P  
3.使用WebAPI实现文件上传 M{X; H'2  
4.如何加强WebAPI的安全性 0w6"p>s>c  
IG4`f~k^  
此部分内容涉及代码下载网盘: :MF+`RpL  
链接: http://pan.baidu.com/s/1o6jZyiQ 9gETWz(3I  
密码:
本部分内容设定了隐藏,需要回复后才能看到
a=*JyZ.2  
@[O|n)7  
S8;5|ya  
3/CKy##r%]  
级别: 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