前言:
在某次公司面试时被问到对GridView操作的熟悉程度,在那之前一直用Repeater内嵌table标签对GridView操作确实很少,于是最近在项目的后台上对GridView进行了一番实操,本文和后面的另一篇GridView实战二:使用ObjectDataSource数据源控件均是这段时间的一些总结。
GridView优点就是集数据绑定、分页、排序、删、改于一身,提高了开发效率;缺点嘛,就是运行效率低,并且它本身不带添加功能。于是GridView用于后台开发是一个不错的选择,而前台页面还是少用为妙咯。
实战一:
 1.本次实战效果图如下:

图一.展示状态

图二.编辑状态

图三.添加记录状态
2.具体代码
.aspx页面
1         <asp:GridView runat="server" ID="gv" AutoGenerateColumns="false"2              AllowPaging="true" PageSize="1" AllowSorting="true" DataKeyNames="ID"3              OnRowDataBound="gv_OnRowDataBound" OnRowDeleting="gv_OnRowDeleting"4              OnRowEditing="gv_OnRowEditing" OnRowCancelingEdit="gv_OnRowCancelingEdit"5              OnRowUpdating="gv_OnRowUpdating" OnSorting="gv_OnSorting">6              <HeaderStyle BackColor="graytext" />7             <Columns>8                 <asp:TemplateField>9                     <HeaderStyle Width="20%" />10                     <HeaderTemplate>11                         <asp:LinkButton runat="server" ID="lbtnSortName" Text="Name" CommandName="Sort" CommandArgument="Name">12                         </asp:LinkButton>13                     </HeaderTemplate>14                     <ItemTemplate><%#Eval("Name") %></ItemTemplate>15                     <EditItemTemplate>16                         <asp:TextBox runat="server" ID="tbxName" Text='<%#Eval("Name") %>'></asp:TextBox>17                         <asp:RegularExpressionValidator runat="server" ID="revName" ControlToValidate="tbxName"18                              ValidationExpression="[a-zA-Z]+" ErrorMessage="Please input your English name!" Display="Dynamic">19                         </asp:RegularExpressionValidator>20                         <asp:RequiredFieldValidator runat="server" ID="rfvName" ControlToValidate="tbxName"21                             ErrorMessage="Please input your name" Display="Dynamic">22                         </asp:RequiredFieldValidator>23                     </EditItemTemplate>24                 </asp:TemplateField>25                 <asp:TemplateField>26                     <HeaderStyle Width="10%"/>27                     <HeaderTemplate>28                         <asp:LinkButton runat="server" ID="lbtnSortSex" Text="Sex" CommandName="Sort" CommandArgument="Sex">29                         </asp:LinkButton>30                     </HeaderTemplate>31                     <ItemTemplate>32                         <asp:RadioButtonList Enabled="false" runat="server" ID="rblSexShow" RepeatDirection="Horizontal"33                             RepeatColumns="2">34                         </asp:RadioButtonList>35                     </ItemTemplate>36                     <EditItemTemplate>37                          <asp:RadioButtonList runat="server" ID="rblSexEdit" RepeatDirection="Horizontal" RepeatColumns="2">38                          </asp:RadioButtonList>39                     </EditItemTemplate>40                 </asp:TemplateField>41                 <asp:TemplateField>42                     <HeaderStyle Width="20%"/>43                     <HeaderTemplate>44                         <asp:LinkButton runat="server" ID="lbtnSortCountry" Text="Country" CommandName="Sort" CommandArgument="Country">45                         </asp:LinkButton>46                     </HeaderTemplate>47                     <ItemTemplate><%#Eval("Country")%></ItemTemplate>48                     <EditItemTemplate>49                         <asp:DropDownList runat="server" ID="ddlCountry"></asp:DropDownList>50                     </EditItemTemplate>51                 </asp:TemplateField>52                 <asp:TemplateField>53                     <HeaderStyle Width="20%"/>54                     <HeaderTemplate>Hobby</HeaderTemplate>55                     <ItemTemplate><%#Eval("Hobby") %></ItemTemplate>56                     <EditItemTemplate>57                         <asp:CheckBoxList runat="server" ID="cbxlHobby" RepeatDirection="Horizontal" RepeatColumns="5">58                         </asp:CheckBoxList>59                     </EditItemTemplate>60                 </asp:TemplateField>61                 <asp:TemplateField ItemStyle-HorizontalAlign="Center">62                     <HeaderStyle Width="10%"/>63                     <HeaderTemplate>64                         <asp:LinkButton runat="server" ID="lbtnShowAdd" Text="Add" OnClick="lbtnShowAdd_OnClick">65                         </asp:LinkButton>66                     </HeaderTemplate>67                     <ItemTemplate>68                         <asp:LinkButton runat="server" ID="lbtnEdit" Text="Edit" CommandName="Edit" /> 69                         <asp:LinkButton runat="server" ID="lbtnDelete" Text="Delete" CommandName="Delete" />70                     </ItemTemplate>71                     <EditItemTemplate>72                         <asp:LinkButton runat="server" ID="lbtnSubmit" Text="Update" CommandName="Update" /> 73                         <asp:LinkButton runat="server" ID="lbtnCancel" Text="Cancel" CommandName="Cancel" />74                     </EditItemTemplate>75                 </asp:TemplateField>76             </Columns>77             <PagerSettings Visible="true" />78             <PagerStyle Font-Size="12px"/>79             <PagerTemplate>80                 <div style="float:left;margin-left:15px;color:#999;line-height:20px">81                     当前第<%#this.gv.PageIndex+1 %>/<%#this.gv.PageCount %>页82                 </div>83                 <div style="float:right;margin-right:15px;color:#999;line-height:20px">页</div>84                 <div style="float:right">85                     <asp:DropDownList runat="server" ID="ddlPaging" AutoPostBack="true"86                         OnSelectedIndexChanged="ddlPaging_OnSelectedIndexChanged">87                     </asp:DropDownList>88                 </div>89                 <div style="float:right;color:#999;line-height:20px">跳转到第</div>90             </PagerTemplate>91             <EmptyDataTemplate>92                 <div><b>Name:</b>93                     <asp:TextBox runat="server" ID="tbxName"></asp:TextBox>94                     <asp:RegularExpressionValidator runat="server" ID="revName" ControlToValidate="tbxName"95                          ValidationExpression="^[a-zA-Z]+$" Display="Dynamic" ErrorMessage="Please input your English name!">96                     </asp:RegularExpressionValidator>97                    <asp:RequiredFieldValidator runat="server" ID="rfvName" ControlToValidate="tbxName"98                         ErrorMessage="Please input your name" Display="Dynamic">99                     </asp:RequiredFieldValidator>100                 </div>101                 <div><b>Sex:</b>102                     <asp:RadioButtonList runat="server" id="rblSex" RepeatColumns="2" RepeatDirection="Horizontal">103                         <asp:ListItem Value="M" Text="Male" Selected="True"></asp:ListItem>104                         <asp:ListItem Value="F" Text="Female"></asp:ListItem>105                     </asp:RadioButtonList>106                 </div>107                 <div><b>Country:</b>108                     <asp:DropDownList runat="server" ID="ddlCountry">109                     </asp:DropDownList>110                     <asp:RequiredFieldValidator runat="server" ID="rfCountry" ControlToValidate="ddlCountry"111                         ErrorMessage="Please Choose Country" Display="Dynamic">112                     </asp:RequiredFieldValidator>113                 </div>114                 <div><b>Hobby:</b>115                     <asp:CheckBoxList runat="server" ID="cbxlHobby" RepeatColumns="5" RepeatDirection="Horizontal">116                     </asp:CheckBoxList>117                 </div>118                 <div>119                     <asp:LinkButton runat="server" ID="lbtnSubmit" Text="Submit" OnClick="lbtnSubmit_OnClick" />120                     <asp:LinkButton  runat="server" ID="lbtnCancel" Text="Cancel" OnClick="lbtnCancel_OnClick"121                         CausesValidation="false"/>122                 </div>123             </EmptyDataTemplate>124         </asp:GridView>说明:
1.显示状态时:对于只显示文字串的用<%#Eval("字段名")%>直接绑定,而对于单选组(性别)的内容就放在GridView的OnRowDataBound来绑定。
2.编辑状态时:复选组、单选组合下拉列表都在OnRowDataBound来绑定;这里没有用数据源控件,所以用<%#Bind("字段名")%>和<%#Eval("字段名")%>没区别,<%#Bind("字段名")%>的双向通讯不起作用。
3.新增状态:因为GridView自身附带新增记录的功能,所以选择在EmptyDataTemplate中实现新增的功能(借鉴其他同行的做法!);因为使用了验证控件,所以把取消按钮(操作不验证合法性)设为不触发验证。
4.分页功能:本例是将分页功能放置到gridview的PagerTemplate中实现。这里有两个注意点:
a.DropDownList设置AutoPostBack为true;
b.因为要触发DropDownList的OnSelectedIndexChanged事件,所以viewstate要启用。触发OnSelectedIndexChanged事件的条件是postback的selectedIndex和原始值不同,当viewstate启用时原始值就是viewstate中保存的值,当viewstate禁用时就是控件初始化时的selectedIndex或第一个选项。因此如果禁用了ViewState那么当选择回控件初始化时的选项时就不会触发OnSelectedIndexChanged事件了。
c.GridView的OnRowDataBound中每次postback都重新初始化DropDownList。因为DropDownList包含在GridView中是动态生成的,当PostBack时GridView并不会恢复其中的动态内容;如果把分页功能放在GridView以外实现,那么动态生成的时DropDownList的选项,就Postback时不用再初始化了。
d.因为DropDownList位于GridView里面,所以当把某个ListItem的enable设为false时,该选项就不生成(连Html代码都没了),如果放在GridView外只是显示为不可用而已,原因不明。为实现添加状态初始画面中存在默认不可选的listitem效果,用了html的disabled属性来设置。
.aspx.cs代码
1 public partial class _Default : System.Web.UI.Page2 {3     private DataManager dm = new DataManager();45     protected void Page_Load(object sender, EventArgs e)6     {7         if (!IsPostBack)8         {9             this.gv.DataSource = dm.GetDt();10             this.gv.DataBind();1112             //记录最近一次排序方向13             ViewState["Direction"] = SortDirection.Descending;14         }15     }1617     protected void gv_OnRowDataBound(object sender, GridViewRowEventArgs e)18     {19         DataRowView drv = e.Row.DataItem as DataRowView;2021         if (e.Row.RowType == DataControlRowType.DataRow)22         {23             //显示时24             if (this.gv.EditIndex == -1)25             {26                 //设置性别27                 RadioButtonList rbl = e.Row.FindControl("rblSexShow") as RadioButtonList;28                 rbl.Items.Add(new ListItem("Male", "M"));29                 rbl.Items.Add(new ListItem("Female", "F"));30                 if ((drv["Sex"] as string).ToLower().Equals("m"))31                     rbl.Items[0].Selected = true;32                 else33                     rbl.Items[1].Selected = true;34             }35             //修改时:36             else if(e.Row.RowIndex == this.gv.EditIndex)37             {38                 //性别39                 RadioButtonList rbl = e.Row.FindControl("rblSexEdit") as RadioButtonList;40                 rbl.Items.Add(new ListItem("Male", "M"));41                 rbl.Items.Add(new ListItem("Female", "F"));42                 if ((drv["Sex"] as string).ToLower().Equals("m"))43                     rbl.Items[0].Selected = true;44                 else45                     rbl.Items[1].Selected = true;46                 //国籍47                 DropDownList ddlCountry = e.Row.FindControl("ddlCountry") as DropDownList;48                 DataTable countryDt = dm.GetCountry();49                 ListItem li = null;50                 for(int i=0;i<countryDt.Rows.Count;++i)51                 {52                     string cn = countryDt.Rows[i]["cn"] as string;53                     li = new ListItem(cn, cn);54                     if (cn.Equals(drv["Country"] as string))55                         li.Selected = true;56                     ddlCountry.Items.Add(li);57                 }58                 //兴趣59                 CheckBoxList cbl = e.Row.FindControl("cbxlHobby") as CheckBoxList;60                 DataTable hobbyDt = dm.GetHobby();61                 string hobbys = drv["Hobby"] as string;62                 ListItem hobbyLi = null;63                 string hstr = string.Empty;64                 for (int i = 0; i < hobbyDt.Rows.Count; i++)65                 {66                     hstr = hobbyDt.Rows[i]["hobby"] as string;67                     hobbyLi = new ListItem(hstr, hstr);68                     if (hobbys.IndexOf(hstr)>=0)69                         hobbyLi.Selected = true;70                     cbl.Items.Add(hobbyLi);71                 }72             }73         }74         else if (e.Row.RowType == DataControlRowType.Pager)75         {76             //绑定分页控件77             DropDownList ddlPaging = e.Row.FindControl("ddlPaging") as DropDownList;78             for (int i = 0; i < this.gv.PageCount; i++)79             {80                 ddlPaging.Items.Add(new ListItem(Convert.ToString(i + 1), Convert.ToString(i)));81             }82             ddlPaging.SelectedIndex = this.gv.PageIndex;83         }84         else if (e.Row.RowType == DataControlRowType.EmptyDataRow)85         {86             //添加记录画面87 //国籍88             DropDownList ddlCountry = e.Row.FindControl("ddlCountry") as DropDownList;89             DataTable countryDt = dm.GetCountry();90             ListItem li =  new ListItem("Please Select","");91             li.Attributes["disabled"] = "disabled";92             li.Selected = true;93             ddlCountry.Items.Add(li);94             for (int i = 0; i < countryDt.Rows.Count; ++i)95             {96                 string cn = countryDt.Rows[i]["cn"] as string;97                 li = new ListItem(cn, cn);98                 ddlCountry.Items.Add(li);99             }100             //兴趣101             CheckBoxList cbl = e.Row.FindControl("cbxlHobby") as CheckBoxList;102             DataTable hobbyDt = dm.GetHobby();103             ListItem hobbyLi = null;104             string hstr = string.Empty;105             for (int i = 0; i < hobbyDt.Rows.Count; i++)106             {107                 hstr = hobbyDt.Rows[i]["hobby"] as string;108                 hobbyLi = new ListItem(hstr, hstr);109                 cbl.Items.Add(hobbyLi);110             }111         }112     }113114     /// <summary>115 /// 分页控件的OnSelectedIndexChanged116 /// </summary>117 /// <param name="sender"></param>118 /// <param name="e"></param>119     protected void ddlPaging_OnSelectedIndexChanged(object sender, EventArgs e)120     {121         this.gv.PageIndex = (sender as DropDownList).SelectedIndex;122         this.gv.DataSource = dm.GetDt();123         this.gv.DataBind();124     }125126     /// <summary>127 /// 删除记录128 /// </summary>129 /// <param name="sender"></param>130 /// <param name="e"></param>131     protected void gv_OnRowDeleting(object sender, GridViewDeleteEventArgs e)132     {133         dm.DelRecord(Convert.ToInt32(this.gv.DataKeys[e.RowIndex].Value));134         this.gv.PageIndex = 0;135         this.gv.DataSource = dm.GetDt();136         this.gv.DataBind();137     }138139     /// <summary>140 /// 修改记录141 /// </summary>142 /// <param name="sender"></param>143 /// <param name="e"></param>144     protected void gv_OnRowEditing(object sender, GridViewEditEventArgs e)145     {146         this.gv.EditIndex = e.NewEditIndex;147         this.gv.DataSource = dm.GetDt();148         this.gv.DataBind();149     }150151     /// <summary>152 /// 取消修改153 /// </summary>154 /// <param name="sender"></param>155 /// <param name="e"></param>156     protected void gv_OnRowCancelingEdit(object sender, GridViewCancelEditEventArgs e)157     {158         this.gv.EditIndex = -1;159         this.gv.DataSource = dm.GetDt();160         this.gv.DataBind();161     }162163     /// <summary>164 /// 更新记录165 /// </summary>166 /// <param name="sender"></param>167 /// <param name="e"></param>168     protected void gv_OnRowUpdating(object sender, GridViewUpdateEventArgs e)169     {170         GridViewRow gvr = this.gv.Rows[e.RowIndex];171         int id = Convert.ToInt32(this.gv.DataKeys[e.RowIndex].Value);172         string name = (gvr.Cells[0].FindControl("tbxName") as TextBox).Text;173         string sex = (gvr.Cells[1].FindControl("rblSexEdit") as RadioButtonList).SelectedValue;174         string country = (gvr.Cells[2].FindControl("ddlCountry") as DropDownList).SelectedValue;175         System.Text.StringBuilder hobbys = new System.Text.StringBuilder();176         foreach (ListItem li in (gvr.Cells[3].FindControl("cbxlHobby") as CheckBoxList).Items)177         {178             if (li.Selected)179                 hobbys.Append(li.Value+",");180         }181         if (hobbys.Length >= 2)182             hobbys.Remove(hobbys.Length - 1, 1);183         dm.UpdateRecord(id, name, country, sex, hobbys.ToString());184185         this.gv.EditIndex = -1;186         this.gv.DataSource = dm.GetDt();187         this.gv.DataBind();188     }189190     /// <summary>191 /// 提交新记录192 /// </summary>193 /// <param name="sender"></param>194 /// <param name="e"></param>195     protected void lbtnSubmit_OnClick(object sender, EventArgs e)196     {197         LinkButton lbtnSubmit = sender as LinkButton;198         GridViewRow tr = lbtnSubmit.NamingContainer as GridViewRow;199         string name = (tr.Cells[0].FindControl("tbxName") as TextBox).Text;200         string sex = (tr.Cells[0].FindControl("rblSex") as RadioButtonList).SelectedValue;201         string country = (tr.Cells[0].FindControl("ddlCountry") as DropDownList).SelectedValue;202         System.Text.StringBuilder hobbys = new System.Text.StringBuilder();203         foreach (ListItem li in (tr.Cells[0].FindControl("cbxlHobby") as CheckBoxList).Items)204         {205             if (li.Selected)206                 hobbys.Append(li.Value + ",");207         }208         if (hobbys.Length >= 2)209             hobbys.Remove(hobbys.Length - 1, 1);210         dm.AddRecord(name,country,sex,hobbys.ToString());211212         this.gv.PageIndex = 0;213         this.gv.DataSource = dm.GetDt();214         this.gv.DataBind();215     }216217     /// <summary>218 /// 退出添加新记录219 /// </summary>220 /// <param name="sender"></param>221 /// <param name="e"></param>222     protected void lbtnCancel_OnClick(object sender, EventArgs e)223     {224         this.gv.DataSource = dm.GetDt();225         this.gv.DataBind();226     }227228     /// <summary>229 /// 显示添加画面230 /// </summary>231 /// <param name="sender"></param>232 /// <param name="e"></param>233     protected void lbtnShowAdd_OnClick(object sender, EventArgs e)234     {235         this.gv.DataSource = null;236         this.gv.DataBind();237     }238239     /// <summary>240 /// 排序241 /// </summary>242 /// <param name="sender"></param>243 /// <param name="e"></param>244     protected void gv_OnSorting(object sender, GridViewSortEventArgs e)245     {246         DataTable dt = dm.GetDt();247         string direction = string.Empty;248         if (e.SortDirection == (SortDirection)Enum.Parse(typeof(SortDirection),Convert.ToString(ViewState["Direction"])))249             direction = "asc";250         else251             direction = "desc";252         dt.DefaultView.Sort = string.Format("{0} {1}", e.SortExpression, direction);253         ViewState["Direction"] = (direction.Equals("asc") ? SortDirection.Descending : SortDirection.Ascending);254         this.gv.DataSource = dt;255         this.gv.DataBind();256     }257 }说明:
1.显示、编辑、新增时数据绑定都集中在gv_OnRowDataBound方法中,通过e.Row.RowType来分别处理。
2.排序:通过DataTable.DefaultView来实现,用一个ViewState变量存放最近一次的排序方向的反方向,来使每次排序的方向都不同。GridView的SortDirection为只读属性,所以别指望设定它来实现排序哦!
3.modify、update、delete和cancel按钮的实现利用GridView预设的CommandName来处理
| CommandName 值 | 说明 | 
| “Cancel” | 取消编辑操作并将 GridView 控件返回为只读模式。引发 RowCancelingEdit 事件。 | 
| “Delete” | 删除当前记录。引发 RowDeleting 和 RowDeleted 事件。 | 
| “Edit” | 将当前记录置于编辑模式。引发 RowEditing 事件。 | 
| “Page” | 执行分页操作。将按钮的 CommandArgument 属性设置为“First”、“Last”、“Next”、“Prev”或页码,以指定要执行的分页操作类型。引发 PageIndexChanging 和 PageIndexChanged 事件。 | 
| “Select” | 选择当前记录。引发 SelectedIndexChanging 和 SelectedIndexChanged 事件。 | 
| “Sort” | 对 GridView 控件进行排序。引发 Sorting 和 Sorted 事件。 | 
| “Update” | 更新数据源中的当前记录。引发 RowUpdating 和 RowUpdated 事件。 | 
当然也可以自己写OnCommand的处理代码来处理,就是烦一点。
数据操作类:
1 public class DataManager2 {3     private static DataTable dt = null;//用户记录4     private static DataTable countryDt = null;//国籍5     private static DataTable hobbyDt = null;//兴趣67     public DataManager()8     {9         if (dt == null)10         {11             dt = new DataTable();12             dt.Columns.Add("ID");13             dt.Columns.Add("Name");14             dt.Columns.Add("Sex");15             dt.Columns.Add("Country");16             dt.Columns.Add("Hobby");1718             //Default Data19             dt.Rows.Add(new object[] { 1,"Mary","F","China","Cooking,Music"});20             dt.Rows.Add(new object[] { 2, "John", "M", "China", "Tennis" });21         }2223         if (countryDt == null)24         {25             countryDt=new DataTable();26             countryDt.Columns.Add("cn");2728             //Default Data29             countryDt.Rows.Add(new object[] { "China" });30             countryDt.Rows.Add(new object[] { "French" });31             countryDt.Rows.Add(new object[] { "America" });32             countryDt.Rows.Add(new object[] { "Afria" });33             countryDt.Rows.Add(new object[] { "Japan" });34         }3536         if (hobbyDt == null)37         {38             hobbyDt = new DataTable();39             hobbyDt.Columns.Add("hobby");4041             //Default Data42             hobbyDt.Rows.Add(new object[] { "Cooking" });43             hobbyDt.Rows.Add(new object[] { "Music" });44             hobbyDt.Rows.Add(new object[] { "Reading" });45             hobbyDt.Rows.Add(new object[] { "Movies" });46             hobbyDt.Rows.Add(new object[] { "Tennis" });47         }48     }4950     public DataTable GetDt()51     {52         return dt;53     }5455     public bool DelRecord(int id)56     {57         DataRow[] drs = dt.Select("ID=" + id);58         bool result = false;59         if (drs.Length == 1)60         {61             dt.Rows.Remove(drs[0]);62             result = true;63         }6465         return result;66     }6768     public bool UpdateRecord(int id, string name, string country, string sex, string hobby)69     {70         DataRow[] drs = dt.Select("ID=" + id);71         bool result = false;72         if (drs.Length == 1)73         {74             drs[0]["Name"] = name;75             drs[0]["Country"] = country;76             drs[0]["Hobby"] = hobby;77             drs[0]["Sex"] = sex;78             result = true;79         }8081         return result;82     }8384     public bool AddRecord(string name, string country, string sex, string hobby)85     {86         DataRow[] drs = dt.Select();87         Array.Sort<DataRow>(drs, new Comparison<DataRow>(SortHandler));88         int newId = 1+Convert.ToInt32(drs[0]["ID"]);89         dt.Rows.Add(new object[] {newId,name,sex,country,hobby });9091         return true;92     }9394     private int SortHandler(DataRow arg1, DataRow arg2)95     {96         if (Convert.ToInt32(arg1["ID"]) == Convert.ToInt32(arg2["ID"]))97             return 0;98         else if (Convert.ToInt32(arg1["ID"]) < Convert.ToInt32(arg2["ID"]))99             return 1;100         else101             return -1;102     }103104     public DataTable GetCountry()105     {106         return countryDt;107     }108109     public DataTable GetHobby()110     {111         return hobbyDt;112     }113 }这里没什么好说的,就是AddRecord方法中用到了Array.Sort方法,这个方法我用得很少,不过觉得挺好用的,定义一个方法作为第二个参数传入就可以了。
总结:
不适用数据源控件的优点是,新增记录时可以马上看到新增的记录;缺点是即使实现了分页功能,但每次还是从数据库中读取全部数据,效率低。而使用ObejctDataSource来绑定就可以解决从数据库读取全部数据的问题,并且附带数据缓存(应用程序级缓存)的功能,省心多了。但却又因ObjectDataSource不支持重新绑定,而使新增记录时不能马上看到新增的记录的缺点。
关于ObjectDataSource配合GridView的使用将在《GridView实战二:使用ObjectDataSource数据源控件》讲述。
欢迎添加我的公众号一起深入探讨技术手艺人的那些事!











