Артем Мурадов
Здравствуйте. Сегодня поговорим о веб-разработке. Несмотря на то, что популярность MVC растет очень быстро, Web Forms ещё никто не отменял. К тому же много приложений написано с использованием Web Forms, да и уже написанные приложения требуют поддержки. И если простейшие контролы (например, TextBox) у новичков вопросов, как правило, не вызывают, то сориентироваться в чем-то более сложном уже проблема.
В данном посте я расскажу в общих чертах, как использовать контрол GridView для работы со списком или специализированным источником данных. Предупреждаю сразу, тема не новая и статья ориентирована на начинающих разработчиков.
Поехали.
Начнем с теории. Класс GridView
Цитата |
Отображает значения источника данных в таблице, где каждый столбец представляет поле, а каждая строка - запись. Элемент управления GridView позволяет выбирать, сортировать и изменять эти записи. |
Его можно связывать с элементами-источниками данных (например, наследниками
DataSourceControl), а можно просто отображать перечисляемые списки, указав значение свойства
DataSource. Начнем с последнего.
Итак, задача: Имеется страница, на которой расположен GridView, и список, который нужно на этой странице формировать и иметь возможность его изменять (то есть добавлять/удалять элементы). Список хранится только на странице.
Порядок действий:
- Создаём веб-приложение
- Удаляем с домашней страницы всё лишнее (необязательно)
- Создаём страницу GridViewList.aspx, указываем для неё мастер страницу
- Добавляем в меню пункт со ссылкой на GridViewList.aspx
Отлично, полигон для действий готов. Класс-объект, с которым мы будем работать, я определил так:
- [Serializable]
- public class People
- {
- [XmlAttribute]
- public int Id { get; set; }
- [XmlAttribute]
- public string FirstName { get; set; }
- [XmlAttribute]
- public string LastName { get; set; }
- }
Теперь определимся со списком. Во-первых, так как список формируется и изменяется только в рамках этой страницы, я решил хранить его во ViewState. Я просто определил свойство на странице, которое автоматом себя сохраняет.
- public List<People> Peoples
- {
- get
- {
- // Получение ключа для поиска во ViewState.
- // hfPeoplesViewState - HiddenField, которое хранит в себе этот ключ
- // Это всё сделано только для того, чтобы обеспечить уникальность ключа на странице.
- // По идее тут можно было просто написать var str = "Peoples_Key" и всё бы работало
- var str = hfPeoplesViewState.Value;
- if (string.IsNullOrEmpty(str))
- {
- // Если ещё ничего не хранит, то создать ключ
- hfPeoplesViewState.Value = string.Format("Peoples_{0}", Guid.NewGuid());
- str = hfPeoplesViewState.Value;
- }
-
- // Проверяю, если во ViewState ещё нет того списка, что мне нужен - создаю его
- if (ViewState[str] == null // !(ViewState[str] is List<People>))
- {
- var peoples = new List<People>
- {
- new People {Id = 1, LastName = "Мурадов", FirstName = "Артем"},
- new People {Id = 2, LastName = "Пупкин", FirstName = "Василий"},
- new People {Id = 3, LastName = "Елопанов", FirstName = "Инокентий"}
- };
- ViewState[str] = peoples;
- return peoples;
- }
- return ViewState[str] as List<People>;
- }
- set
- {
- // Аналогично методу get
- var str = hfPeoplesViewState.Value;
- if (string.IsNullOrEmpty(str))
- {
- hfPeoplesViewState.Value = string.Format("Peoples_{0}", Guid.NewGuid());
- str = hfPeoplesViewState.Value;
- }
-
- ViewState[str] = value;
- }
- }
Добавим GridView в разметку. По пути также добавим пару текстовых полей и кнопку, для возможности добавления элемента в коллекцию.
- <asp:TextBox runat="server" ID="tbFirstName">
- </asp:TextBox>
- <asp:TextBox runat="server" ID="tbLastName">
- </asp:TextBox>
- <asp:Button runat="server" OnClick="BtAddPeople" Text="+" />
- <hr />
- <asp:GridView runat="server" ID="gv" AutoGenerateColumns="True" Width="60%">
- <EmptyDataTemplate>
- Записей нет</EmptyDataTemplate>
- </asp:GridView>
В обработчике событий
- protected void Page_Load(object sender, EventArgs e)
- {
- if (!IsPostBack)
- {
- gv.DataSource = Peoples;
- gv.DataBind();
- }
- }
-
- protected void BtAddPeople(object sender, EventArgs e)
- {
- if (!string.IsNullOrEmpty(tbFirstName.Text) && !string.IsNullOrEmpty(tbLastName.Text))
- {
- var id = Peoples.Count > 0 ? Peoples.Max(x => x.Id) + 1 : 1;
- var p = new People
- {
- Id = id,
- FirstName = tbFirstName.Text,
- LastName = tbLastName.Text
- };
- Peoples.Add(p);
- UpdateGrid();
- }
- }
-
- private void UpdateGrid()
- {
- gv.DataSource = Peoples;
- gv.DataBind();
- }
Окей, теперь при первой загрузке страницы, у нас будет заполняться GridView. А при добавлении элемента в коллекцию, GridView будет обновляться. Вот как это выглядит:
Добавление работает, но у нас нет ни сортировки, ни редактирования, ни удаления, да ещё и включена автогенерация полей. Вот с последнего и начнем. Отключаем автогенерацию, указываем нужные нам поля, разрешаем сортировку (список возможных полей):
- <asp:GridView runat="server" ID="gv" AutoGenerateColumns="False" Width="60%" AllowSorting="True">
- <Columns>
- <asp:BoundField HeaderText="ИД" DataField="Id" SortExpression="Id" ReadOnly="True">
- <ItemStyle Width="5%"></ItemStyle>
- </asp:BoundField>
- <asp:BoundField HeaderText="Имя" DataField="FirstName" SortExpression="FirstName">
- <ItemStyle Width="40%"></ItemStyle>
- </asp:BoundField>
- <asp:BoundField HeaderText="Фамилия" DataField="LastName" SortExpression="LastName">
- <ItemStyle Width="40%"></ItemStyle>
- </asp:BoundField>
- <asp:CommandField ShowEditButton="True" ShowDeleteButton="True" ShowCancelButton="True">
- <ItemStyle Width="15%"></ItemStyle>
- </asp:CommandField>
- </Columns>
- <EmptyDataTemplate>
- Записей нет</EmptyDataTemplate>
- </asp:GridView>
Теперь у нас есть кнопки, для того, чтобы сортировать, редактировать или удалять. Но только этого функционала пока нет. GridView не может самостоятельно править нашу коллекцию. Но это не беда, нам достаточно подписаться на нужные нам события и реализовать всё самим. Для редактирования нужны следующие события: OnRowEditing="GvEditing" OnRowUpdating="GvUpdating" OnRowCancelingEdit="GvCancelingEdit", для сортировки OnSorting="GvSorting", для удаления OnRowDeleting="GvDeleting".
- <asp:GridView runat="server" ID="gv" AllowSorting="True" OnSorting="GvSorting" OnRowEditing="GvEditing"
- OnRowUpdating="GvUpdating" OnRowCancelingEdit="GvCancelingEdit" OnRowDeleting="GvDeleting"
- DataKeyNames="Id" AutoGenerateColumns="False" Width="60%" >
- <Columns>
- <asp:BoundField HeaderText="ИД" DataField="Id" SortExpression="Id" ReadOnly="True">
- <ItemStyle Width="5%"></ItemStyle>
- </asp:BoundField>
- <asp:BoundField HeaderText="Имя" DataField="FirstName" SortExpression="FirstName">
- <ItemStyle Width="40%"></ItemStyle>
- </asp:BoundField>
- <asp:BoundField HeaderText="Фамилия" DataField="LastName" SortExpression="LastName">
- <ItemStyle Width="40%"></ItemStyle>
- </asp:BoundField>
- <asp:CommandField ShowEditButton="True" ShowDeleteButton="True" ShowCancelButton="True">
- <ItemStyle Width="15%"></ItemStyle>
- </asp:CommandField>
- </Columns>
- <EmptyDataTemplate>
- Записей нет</EmptyDataTemplate>
- </asp:GridView>
Вот код для сортировки
- /// <summary>
- /// Возникает при сортировке в гриде.
- /// </summary>
- protected void GvSorting(object sender, GridViewSortEventArgs e)
- {
- var p = Peoples;
-
- // Необходимо определить, по какому именно полю сортировать
- if (e.SortExpression == "LastName")
- {
- // используем вспомогательный метод Sort
- p = Sort("LastName", list => list.OrderBy(x => x.LastName).ToList(),
- list => list.OrderByDescending(x => x.LastName).ToList(), p);
- }
- if (e.SortExpression == "FirstName")
- {
- p = Sort("FirstName", list => list.OrderBy(x => x.FirstName).ToList(),
- list => list.OrderByDescending(x => x.FirstName).ToList(), p);
- }
- if (e.SortExpression == "Id")
- {
- p = Sort("Id", list => list.OrderBy(x => x.Id).ToList(),
- list => list.OrderByDescending(x => x.Id).ToList(), p);
- }
-
- Peoples = p;
- // обновление грида
- UpdateGrid();
- }
-
- /// <summary>
- /// Вспомогательный метод для сортировки
- /// </summary>
- private List<People> Sort(string column, Func<List<People>, List<People>> ascFunc, Func<List<People>, List<People>> descFunc, List<People> data)
- {
- List<People> result;
- // Если в прошлвй раз была сортировка по этому же полю
- if (hfLastSortFieldState.Value == column)
- {
- // смотрим, в каком направлении была сортировка в прошлвй раз и сортируем в другом
- result = hfLastSortDirectionState.Value != "ASC" ? ascFunc(data) : descFunc(data);
- // сохраняем колонку и направление сортировки
- hfLastSortDirectionState.Value = hfLastSortDirectionState.Value != "ASC" ? "ASC" : "DESC";
- hfLastSortFieldState.Value = column;
- }
- else
- {
- // Если это поле ещё не сортировано, сортируем по возрастанию
- result = ascFunc(data);
- // сохраняем колонку и направление сортировки
- hfLastSortDirectionState.Value = "ASC";
- hfLastSortFieldState.Value = column;
- }
-
- return result;
- }
Код для редактирования и удаления
- /// <summary>
- /// Событие удаления элемента. Поле Id попадает в набор ключей,
- /// так как в гриде указано DataKeyNames="Id"
- /// Нам осталось только определить ключ и убрать элемент с этим ключем из коллекции
- /// </summary>
- protected void GvDeleting(object sender, GridViewDeleteEventArgs e)
- {
- int id;
- // Определяем идентификатор
- if (int.TryParse(string.Format("{0}", e.Keys["Id"]), out id))
- {
- // Убираем из списка
- Peoples = Peoples.Where(x => x.Id != id).ToList();
- // обновляем грид
- UpdateGrid();
- }
- }
-
- /// <summary>
- /// Событие возникает, когда пользователь начинает редактировать строку.
- /// Необходимо просто указать гриду индекс редактируемой строки и обновить грид
- /// </summary>
- protected void GvEditing(object sender, GridViewEditEventArgs e)
- {
- gv.EditIndex = e.NewEditIndex;
- UpdateGrid();
- }
-
- /// <summary>
- /// Событие возникает, когда пользователь обновляет строку.
- /// Необходимо определить Id строки, получить элемент по этому Id, обновить поля и обновить грид
- /// </summary>
- protected void GvUpdating(object sender, GridViewUpdateEventArgs e)
- {
- int id;
- if (int.TryParse(string.Format("{0}", e.Keys["Id"]), out id))
- {
- Peoples
- .Where(x => x.Id == id)
- .ToList()
- .ForEach(x =>
- {
- // я использую string.Format, так как
- // e.NewValues["FirstName"] имеет тип object
- // и может быть равен null
- x.FirstName = string.Format("{0}", e.NewValues["FirstName"]);
- x.LastName = string.Format("{0}", e.NewValues["LastName"]);
- });
- gv.EditIndex = -1;
- UpdateGrid();
- }
- }
-
- /// <summary>
- /// Событие возникает, когда пользователь отменяет обновление строки
- /// Нужно установить gv.EditIndex = -1; и обновить грид
- /// </summary>
- protected void GvCancelingEdit(object sender, GridViewCancelEditEventArgs e)
- {
- gv.EditIndex = -1;
- UpdateGrid();
- }
В итоге имеем грид, элементы которого можем удалять, добавлять, редактировать и сортировать.
Это всё хорошо, но что делать, если мы работаем с базой данных? В таких случаях можно определять источники данных (например, наследники DataSourceControl) и связывать их с гридом. Я приведу пример с использованием ObjectDataSource. Я выбрал ObjectDataSource, так как он зависит только от вашего кода и ему всё равно, откуда берутся данные - из базы данных, сервиса, файла, да чего угодно.
Формализуем задачу. Необходимо реализовать такой же функционал, как и в предыдущем примере, но с использованием ObjectDataSource. Только на этот раз данные я буду хранить не на странице, а в статической переменной - чтобы эти данные были доступны в рамках всего приложения (ведь так и происходит, когда вы работаете, например, с базой данных).
Вот класс автомобиля, с которым будем работать
- public class Car
- {
- public int Id { get; set; }
- public string Manufacturer { get; set; }
- public string Model { get; set; }
- }
Начнем с класса-репозитория. Именно его методы будет вызывать ObjectDataSource для получения данных и манипуляции с ними. Хочу заметить, что я подобрал сигнатуру и набор методов репозитория специально для использования в ObjectDataSource. Вот сам класс:
- public class CarRepository
- {
- /// <summary>
- /// Статическая переменная. В ней будем хранить наши данные
- /// </summary>
- private static List<Car> _store;
-
- /// <summary>
- /// Статический конструктор. Внутри инициализируем наши данные
- /// </summary>
- static CarRepository()
- {
- _store = new List<Car>();
-
- for (var i = 0; i < 1000; i++)
- {
- _store.Add(new Car
- {
- Id = i,
- Manufacturer = "Manufacturer" + i,
- Model = "Model" + i
- });
- }
- }
-
- /// <summary>
- /// Получение данных. Принимает три параметра. Назначение первых двух очевидно,
- /// а вот в третьем приходит информация для сортировки. Например, у нас сортировка по полю
- /// "Id". При сортировке по возрастанию переметр sort будет равен "Id".
- /// При сортировке по убыванию - "Id DESC"
- /// </summary>
- public IEnumerable<Car> GetCars(int maximumRows, int startRowIndex, string sort)
- {
- IEnumerable<Car> temp = _store;
-
- // Сперва определяем направление сортировки, затем поле, по которому сортируем
- if (sort.Contains("DESC"))
- {
- if (sort.Contains("Id")) temp = _store.OrderByDescending(x => x.Id);
- if (sort.Contains("Manufacturer")) temp = _store.OrderByDescending(x => x.Manufacturer);
- if (sort.Contains("Model")) temp = _store.OrderByDescending(x => x.Model);
- }
- else
- {
- if (sort.Contains("Id")) temp = _store.OrderBy(x => x.Id);
- if (sort.Contains("Manufacturer")) temp = _store.OrderBy(x => x.Manufacturer);
- if (sort.Contains("Model")) temp = _store.OrderBy(x => x.Model);
- }
-
- return temp.Skip(startRowIndex).Take(maximumRows);
- }
-
- /// <summary>
- /// Необходим для подсчёта общего количества элеменов.
- /// Используется для постраничного вывода данных
- /// </summary>
- public int Count()
- {
- return _store.Count;
- }
-
- /// <summary>
- /// Для обновления в метод приходит экземпляр Car с
- /// идентификатором обновляемой машины и новыми значениями полей
- /// </summary>
- public void Update(Car car)
- {
- _store
- .Where(x => x.Id == car.Id)
- .ToList()
- .ForEach(x =>
- {
- x.Manufacturer = car.Manufacturer;
- x.Model = car.Model;
- });
- }
-
- /// <summary>
- /// Удаление. Код и так понятен.
- /// </summary>
- public void Delete(Car car)
- {
- _store = _store.Where(x => x.Id != car.Id).ToList();
- }
-
- /// <summary>
- /// Добавление
- /// </summary>
- public void Insert(Car car)
- {
- car.Id = _store.Count > 0 ? _store.Max(x => x.Id) + 1 : 1;
- _store.Add(car);
- }
- }
Отлично. Теперь, как и в предыдущем примере, добавляем страницу в проект и кидаем на эту страницу 2 текстбокса и кнопку (для возможности добавления)
- <asp:TextBox runat="server" ID="tbManufacturer">
- </asp:TextBox>
- <asp:TextBox runat="server" ID="tbModel">
- </asp:TextBox>
- <asp:Button runat="server" OnClick="BtAddCar" Text="+" />
- <hr />
- protected void BtAddCar(object sender, EventArgs e)
- {
- if (!string.IsNullOrEmpty(tbManufacturer.Text) && !string.IsNullOrEmpty(tbModel.Text))
- {
- var carRepository = new CarRepository();
- carRepository.Insert(new Car {Manufacturer = tbManufacturer.Text, Model = tbModel.Text});
- gv.DataBind();
- }
- }
Теперь поработаем над самим контролом ObjectDataSource.
- <asp:ObjectDataSource ID="ods" runat="server" TypeName="GridViewTest.CarRepository"
- DataObjectTypeName="GridViewTest.Car" EnablePaging="True" MaximumRowsParameterName="maximumRows"
- StartRowIndexParameterName="startRowIndex" SortParameterName="sort" InsertMethod="Insert"
- SelectCountMethod="Count" SelectMethod="GetCars" UpdateMethod="Update" DeleteMethod="Delete">
- </asp:ObjectDataSource>
Поясню по каждому полю.
Теперь у нас всё готово для того, чтобы добавить на страницу грид. Приведу весь его код.
- <asp:GridView runat="server" ID="gv" AllowSorting="True" DataKeyNames="Id" AutoGenerateColumns="False"
- DataSourceID="ods" Width="60%" AllowPaging="True" PageSize="20">
- <Columns>
- <%--Тут, как и в предыдущем примере, я использую BoundField--%>
- <asp:BoundField HeaderText="ИД" DataField="Id" ReadOnly="True" SortExpression="Id">
- <ItemStyle Width="5%"></ItemStyle>
- </asp:BoundField>
- <%--Здесь я тоже мог бы использовать BoundField, но я хотел показать, как использовать
- TemplateField с односторонней (Eval) и двухсторонней (Bind) привязкой данных--%>
- <asp:TemplateField HeaderText="Производитель" SortExpression="Manufacturer">
- <ItemStyle Width="35%"></ItemStyle>
- <ItemTemplate>
- <%#Eval("Manufacturer")%>
- </ItemTemplate>
- <EditItemTemplate>
- <asp:TextBox runat="server" ID="tbMan" Text='<%#Bind("Manufacturer") %>'></asp:TextBox>
- </EditItemTemplate>
- </asp:TemplateField>
- <%--Тут всё аналогично предыдущему столбцу--%>
- <asp:TemplateField HeaderText="Модель" SortExpression="Model">
- <ItemStyle Width="35%"></ItemStyle>
- <ItemTemplate>
- <%#Eval("Model")%>
- </ItemTemplate>
- <EditItemTemplate>
- <asp:TextBox runat="server" ID="tbModel" Text='<%#Bind("Model") %>'></asp:TextBox>
- </EditItemTemplate>
- </asp:TemplateField>
- <%--Я хочу показать, что для того, чтобы использовать кнопки редактирования/удаления,
- вам не обязательно пользоваться CommandField. Достаточно указать нужное значение в поле
- CommandName любого кнопочного контрола--%>
- <asp:TemplateField>
- <ItemStyle Width="25%"></ItemStyle>
- <ItemTemplate>
- <asp:LinkButton runat="server" CommandName="Edit" Text="Изменить"></asp:LinkButton>
- <%--А раз это простые кнопки, то можно им добавлять нужное поведение. Например,
- OnClientClick отработает на клиенте, спросит у пользователя разрешение на действие,
- и постбек произойдет только в том случае, если пользователь ответит утвердительно --%>
- <asp:LinkButton runat="server" CommandName="Delete" Text="Удалить"
- OnClientClick="return confirm('Вы действительно хотите удалить запись?');"></asp:LinkButton>
- </ItemTemplate>
- <EditItemTemplate>
- <asp:LinkButton runat="server" CommandName="Update" Text="Обновить"></asp:LinkButton>
- <asp:LinkButton runat="server" CommandName="Cancel" Text="Отменить"></asp:LinkButton>
- </EditItemTemplate>
- </asp:TemplateField>
- </Columns>
- <%--Контент, который будет показан, если записей не будет вовсе--%>
- <EmptyDataTemplate>
- Записей нет</EmptyDataTemplate>
- </asp:GridView>
Собрав всё вместе, получаем результат:
Вот и всё. Данные можно изменять, сортировать, удалять.
Таким образом, используя GridView и различные источники данных, можно легко манипулировать данными и представлять их в удобном для вас виде.
Исходный код солюшена