Как работать с массивами структурированных данных, часть I

Думал влезть в одну статью, но не влез. Так что будет две.

Первая (то бишь эта) – про то как создать собственный класс для хранения данных, как создать набор этим данных, как его правильно отображать на форме, как добавлять новые записи, как сохранять и загружать из файла.

Вторая часть – про то как вместо кнопок использовать меню, как удалять/редактировать записи, как писать запросы.

Так как фантазия у меня скудная, я буду хранить данные о планетах солнечной системы, их немного и их легко запомнить [вообще для БД это так себе тема, так как новые планеты появляются редко, и следовательно записей будет мало, а новых записей придется ждать ни один миллион лет].

Короче, создам класс в котором не будет ни одной функции, а будут исключительно данные. То есть вот раньше (во 2-ой лабе) мы использовали класс для объединения всей логики программы под одним именем Logic, и там были только функции. В лабе 3 у нас был класс и с функциями и сданным. А тут мы объединим несколько переменных под одним именем и хранить в виде одной сущности без функций то есть так называемый класс данных.

Создем новое приложение Windows Form Application. Пишем:

//...

namespace WindowsFormsApp8
{
    // это пока не трогаем
    public partial class Form1 : Form
    {
      // ...
    }
    
    // а тут добавили класс
    public class SolarSystemObject
    {
        public string Name { get; set; } // имя планеты
        public float DistanceToTheSun { get; set; } // среднее расстояние до солнца
        public float DayLengthInDays { get; set; } // сколько длится день на планете в земных сутках
        public float YearLengthInDays { get; set; } // сколько длится год на планете в земных сутках
        public float Radius { get; set; } // радиус планеты в километрах
    }
}

Зачем мы после каждого поля написали { get; set; }? Таким образом мы явно показали, что это поле не просто переменная внутри класса, а целое Свойство (это нам пригодится в дальнейшем).

Давайте теперь построим форму. добавим на нее

  • DataGridView, назовем его dataGridView (то есть свойство установим (Name))

теперь переключимся на код формы (нажмем F7).

Мы хотим, чтобы в dataGridView отображалась информацию об объектах солнечной системы в табличной форме. Чтобы было что отображать надо эти данные откуда-то брать. Давайте добавим поле в класс Form1 для хранения списка объектов.

namespace WindowsFormsApp8
{
    public partial class Form1 : Form
    {
        // добавили поле, для хранения объектов
        List<SolarSystemObject> solarSystemObjects = new List<SolarSystemObject>();

        public Form1()
        {
            InitializeComponent();
        }
    }
    
    // ...
}

Куда хранить данные есть, подключим список к dataGridView следующим образом:

namespace WindowsFormsApp8
{
    public partial class Form1 : Form
    {
        List<SolarSystemObject> solarSystemObjects = new List<SolarSystemObject>();

        public Form1()
        {
            InitializeComponent();
            dataGridView.DataSource = solarSystemObjects; // подключаем
        }
    }
    
    // ...
}

Можно запустить и проверить:

Тут видно, что, хотя мы и привязали пустой список к таблице, она догадалась какие столбцы необходима создать. Догадалась, посмотрев какие столбцы свойства мы создали в нашем классе (кстати тут и понадобилась конструкция {get; set;})

То есть можно попробовать убрать у какого-нибудь поля конструкцию {get; set;}, например так:

//...

namespace WindowsFormsApp8
{
    public partial class Form1 : Form
    {
      // ...
    }
    
    public class SolarSystemObject
    {
        public string Name; // убрал тут { get; set; }
        public float DistanceToTheSun { get; set; }
        public float DayLengthInDays { get; set; }
        public float YearLengthInDays { get; set; }
        public float Radius { get; set; }
    }
}

Вернем все обратно,

//...

public class SolarSystemObject
{
    public string Name { get; set; } // вернули
    public float DistanceToTheSun { get; set; }
    public float DayLengthInDays { get; set; }
    public float YearLengthInDays { get; set; }
    public float Radius { get; set; }
}

Делаем кнопку для добавление записи

Правим форму, добавляем кнопку:

А теперь добавим обработчик события этой кнопки (дважды щелкнув по ней)

// ...

private void button1_Click_1(object sender, EventArgs e)
{
    // хитрый способ создания, с указанием всех его публичных свойств
    // без необходимости явного создания конструктора
    var solarObject = new SolarSystemObject
    {
        DayLengthInDays = 1,
        DistanceToTheSun = 149600000,
        Name = "Земля",
        Radius = 6371,
        YearLengthInDays = 365.24222f
    };

    solarSystemObjects.Add(solarObject);
}

// ...

Попробуем запустить:

Чет не тыкается…

Привязываем данные правильно

Чтобы кнопка начала работать, надо сначала понять почему наша конструкция сейчас не работает.

Связанно это с тем что dataGridView.DataSource достаточно высокоуровневый объект и привязывая к нему список или какой-то другой объект, этот самый DataSource ожидает что его будут оповещать об изменениях, чтоб в случае чего оповестить dataGridView, что ему надо табличку перерисовать. То есть, по-хорошему, при добавлении элемента в список, список должен сообщить DataSource, что он изменился. Однако List один из простейших классов в C# и делать этого не умеет. Поэтому добавляя элемент в список, визуально ничего не меняется.

Один из способов заставить DataSource перегрузить данные, это явно сначала отвязать данные, а потом снова привязать, как-то так:

private void button1_Click_1(object sender, EventArgs e)
{
    // ...

    solarSystemObjects.Add(solarObject);
    dataGridView.DataSource = null; // отвязали
    dataGridView.DataSource = solarSystemObjects; // привязали
}

Работает:

Правда такой способ в природе называется костылем. И C# предлагает более элегантное решение. В виде замены простого списка List, на специально адаптированный под работы с привязанными данными BindingList.

Заменим List на BindingList:

namespace WindowsFormsApp8
{
    public partial class Form1 : Form
    {
        BindingList<SolarSystemObject> solarSystemObjects = new BindingList<SolarSystemObject>();
        // ...
    }
}

и приведём обработчик клика у кнопки к изначальному виду:

// ...

private void button1_Click_1(object sender, EventArgs e)
{
    var solarObject = new SolarSystemObject
    {
        DayLengthInDays = 1,
        DistanceToTheSun = 149600000,
        Name = "Земля",
        Radius = 6371,
        YearLengthInDays = 365.24222f
    };

    solarSystemObjects.Add(solarObject);
}

// ...

Запускаем:

И снова все работает, а код куда короче и красивее.

Делаем красивые заголовки

Сейчас у нас текст заголовков выводится в соответствии с именами свойств, что выглядит не красиво. Да и программу мы пишем для потенциального русского потребителя. Чтобы указать какой текст использовать для свойств, вопсользуемся следующим способом: переключаемся на форму, выделим dataGridView и:

далее

выбираем Объект и кликаем далее

разворачиваем дерево объектов и выбираем наш класс SolarSystemObject

жмем готово и нас снова на форму перекинет, там кликаем на Правку столбцов…:

откроется редактор столбцов, выбираем любой столбец

и отредактируем свойство HeaderText

можно закрыть редактор столбцов и увидеть, что заголовок действительно поменялся:

Если у всех столбцов прописать корректное свойство HeaderText, будет выглядеть как-то так:

и табличка:

Делаем форму для добавления записи

Убираем упрощалки

У нас уже есть кнопка нажимаю на которую добавляется запись с информацией о земле, было б здорово чтобы была возможность указать все поля руками. Возможно вы уже заметили, что в момент замены List на BindingList стало возможно редактировать поля таблицы. Да что там редактировать, даже добавлять:

это конечно здорово, но никаких навыков это нам не дает. Да и редактирование данных в таблицах напрямую часто является плохой практикой в реальных производственных процессах. Да и, в принципе, о чем тогда тут писать…

В общем, чтобы это не смущало наши неокрепшие умы, пойдем на форму выделим наш dataGridView и отключим все эти штуковины

Отлично, теперь мы в спартанских условиях, ничего не редактируется и не добавляется, только через кнопку:

Добавляем новую форму

Я хочу, чтобы при нажатии на кнопку Добавить, открывалось новое окошко куда можно ввести все данные, также чтобы там была кнопка Ok по нажатию на которую окно закрывается и в табличку добавляется новая запись с введенными значениями.

Чтобы добавить новое окно, надо добавить новую форму. Одна форма всегда создается по умолчанию при создании Windows Form Application, если же хочется создать новую форму то делаем следующее.

Тыкаем правой кнопкой мыши на название проекта в обозревателе решений и выбираем пункты контекстного меню как на картинке:

Далее откроется окно создание нового объекта, пропишем имя новой формы SolarObjectForm.cs и жмем добавить:

Откроется редактор новосозданной формы, а в обозревателе решение теперь будет видно сразу две формы

Реализуем открытие новой формы по нажатию кнопки

Давайте пока забудем зачем мы создавали форму, и просто попробуем ее показать по нажатию кнопки. Чтобы точно не спутать ее со старой формой добавим на нее чего-нибудь. Как-нибудь так:

Теперь переключимся на старую форму (кликнув ее два раза в обозревателе решений), кликнем дважды на кнопку чтобы попасть в обработчик события клика, и отчистим его

private void button1_Click_1(object sender, EventArgs e)
{
    // этот код нам больше не нужен
    /*var solarObject = new SolarSystemObject
    {
        DayLengthInDays = 1,
        DistanceToTheSun = 149600000,
        Name = "Земля",
        Radius = 6371,
        YearLengthInDays = 365.24222f
    };

    solarSystemObjects.Add(solarObject);*/
}

перепишем метод, добавим код для отображения формы:

private void button1_Click_1(object sender, EventArgs e)
{
    var solarObjectForm = new SolarObjectForm(); // создаем экземпляр формы
    solarObjectForm.ShowDialog(); // показываем в виде модального окно, 
                                  // модальное окно в отличие от простого окна блокирует доступ к родительскому окну
                                  // то есть в нашем случае к главной форме с табличкой
                                  // пока окно не закроется сделать с главной формой ничего нельзя
}

проверяем:

Добавляем поля на форме

Открываем нашу новую форму SolarObjectForm, добавляем поля для ввода и называем их

  • txtName “для Названия”
  • txtDistanceToTheSun для “Расстояние до солнца”
  • txtDayLengthInDays для “Продолжительность дня в земных сутках”
  • txtYearLengthInDays для “Продолжительность года в земных сутках”
  • txtRadius для “Радиус, км”

и еще две кнопки Ok и “Отмена”, выглядеть будет как на картинке:

Запустим приложение и все должно работать как должно:

Используем форму чтобы добавить новую запись

Есть несколько подходов реализовать добавление новой записи через форму с последующем добавлением ее в табличку. Я буду использовать следующую схему:

  1. При нажатии на кнопку Добавить создам экземпляр класса SolarObject(далее объект)
  2. Передам этот объект на заполнение ново-созданной форме (спасибо ссылочным типам, сделать это будет очень легко)
  3. Если пользователь нажмет Ok добавлю объект в список
  4. А если нажмет Отмену то ничего делать не буду и просто выйду из обработчика

Чтобы иметь возможность передать новой форме какой-то объект надо этой форме добавить поле где будет хранится ново-созданный объект (ровно так как мы делали в 3-ей лабе). Правим код формы SolarObjectForm:

namespace WindowsFormsApp8
{
    public partial class SolarObjectForm : Form
    {
        public SolarSystemObject SolarObjectField; // добавили объект

        public SolarObjectForm()
        {
            InitializeComponent();
        }
    }
}

теперь реализуем 1-ый и 2-ой пункты нашей схемы, переключимся на старую форму, и откроем код обработчика клика на кнопку “добавить”

private void button1_Click_1(object sender, EventArgs e)
{
    var solarObjectForm = new SolarObjectForm();
    
    // 1. создам экземпляр класса **SolarObject**
    var solarObject = new SolarSystemObject();
    // 2. Передам этот **объект** на заполнение ново-созданной форме
    solarObjectForm.SolarObjectField = solarObject;

    solarObjectForm.ShowDialog();
}

если запустить программу пока никаких изменений не будет видно. Давайте теперь переключимся на новую форму, и добавим обработчик клика для кнопки Ok (дважды щелкнем ее).

Я хочу, чтобы по нажатию на кнопку все введенные на форме значения сохранились в полеSolarObjectField, пишем код

namespace WindowsFormsApp8
{
    public partial class SolarObjectForm : Form
    {
        // ...

        // !!!НЕ ПЕРЕПУТАЙТЕ, ЭТО ОБРАБОТЧИК КНОПКИ НА НОВОЙ ФОРМЕ
        private void button1_Click(object sender, EventArgs e) 
        {
            try
            {
                SolarObjectField.Name = txtName.Text;
                SolarObjectField.DayLengthInDays = float.Parse(txtDayLengthInDays.Text);
                SolarObjectField.YearLengthInDays = float.Parse(txtYearLengthInDays.Text);
                SolarObjectField.Radius = float.Parse(txtRadius.Text);
                SolarObjectField.DistanceToTheSun = float.Parse(txtDistanceToTheSun.Text);
            }
            catch (FormatException) // чтоб программа не падала если ввели некорректный данные
            {
                // чтобы модальное окно не закрывалось при некорректных данных
                this.DialogResult = DialogResult.None; 
                return;
            }
        }
    }
}

можно запустить форму, понажимать на кнопку, и вновь никаких изменений не будет заметно.

Давайте теперь сделаем чтобы нажатие на кнопку не только сохраняла введенные значения в объект, но и закрывало форму.

Чтобы кнопка еще и закрывала форму, надо выбрать кнопку Ok, найти в свойствах кнопки DialogResult и выбрать из выпадающего списка значение OK

теперь запускаем и проверяем, что форма закрывается по нажатию кнопки

Осталось теперь чтобы введенные значения заносились в табличку.

Переключимся на старую форму и поправим обработчик клика на кнопку Добавить:

private void button1_Click_1(object sender, EventArgs e)
{
    var solarObjectForm = new SolarObjectForm();
    
    var solarObject = new SolarSystemObject();
    solarObjectForm.SolarObjectField = solarObject;
    
    // если нажали кнопку OK (именно для этого мы кнопке поставили свойство DialogResul)
    if (solarObjectForm.ShowDialog() == DialogResult.OK) 
    {
        this.solarSystemObjects.Add(solarObject); // добавляем объект в список
    }
}

запускаем и проверяем:

красота!

Реализуем сохранение содержимого таблички в файл

Есть масса способов как сохранять данные в файл, один их этих способов вы уже реализовывали при выполнении 4-ой лабы. То есть брали содержимое текстового поля, которое в начале заполняли, и сохраняли в виде текста в файл.

Такой способ хорош, когда у вас простые данные, но с ростом сложности использовать его становится крайне неэффективно. Так что мы будем использовать более прогрессивный метод, именуемый Сериализацией.

Если вам лень тыкать ссылку, то сходив по ней вы бы узнали, что Сериализация – это процесс перевода какой-либо структуры данных в любой другой, более удобный для хранения формат.

Что это значит для нас? Для нас это значит, что путем нехитрой манипуляции с кодом, мы сможем превращать наш объект типа SolarSystemObject в вид пригодной для сохранения в текстовый файл всего парой сотен строк [шучу, штук 10 нам хватит].

Первое что надо сделать, это пометить наш класс SolarSystemObject как сериализуемый, вот так:

[Serializable] // << добавил 
public class SolarSystemObject
{
    public string Name { get; set; }  // имя планеты
    public float DistanceToTheSun { get; set; } // среднее расстояние до солнца
    public float DayLengthInDays { get; set; } // сколько длится день на планете в земных сутках
    public float YearLengthInDays { get; set; } // сколько длится год на планете в земных сутках
    public float Radius { get; set; } // радиус планеты в километрах
}

теперь добавим кнопку, которая позволит нам сохранять содержимое в файл:

кликнем на нее дважды чтобы добавить обработчик события клика, и напишем в него следующий код:

using System;
// ...
using System.Xml.Serialization; // добавил новый using для сериализации в формат XML
using System.IO; // добавил новый using для более низкоуровневого доступа к файлам

namespace WindowsFormsApp8
{
    public partial class Form1 : Form
    {
        public BindingList<SolarSystemObject> solarSystemObjects { get; set; } = new BindingList<SolarSystemObject>();

        // ...

        private void button2_Click(object sender, EventArgs e)
        {
            // создаю окно диалога для выбора файла для сохранения
            // в отличии от OpenFileDialog позволяет выбирать не существующие файлы
            var saveFileDialog = new SaveFileDialog();
            if (saveFileDialog.ShowDialog() != DialogResult.OK) // проверяем что файл выбрали и нажали ok
                return;
        
            // создаю сериализатор в XML, то есть класс у которого есть функция,
            // которая умеет преобразовывать классы, помеченные [Serializable]
            // в текстовые файлы особого вида, так называемые XML-ки (экс-эм-эльки)
            // указываем что хотим хранить массив объектов типа SolarSystemObject
            var serializer = new XmlSerializer(typeof(SolarSystemObject[]));
            
            // страшное заклинание для открытия файла saveFileDialog.FileName,
            // для доступа к файлу используется класс FileStream 
            // (кстати, FileStream переводится как "файловый поток")
            // 
            // FileMode.OpenOrCreate -- указание как работать с файлом,
            // типа, если есть файл, то открой его, и перезапиши все что в нем есть,
            // а если файла нет, то создай сначала, а потом открой.
            // 
            // Зачем тут какой-то using? Это правила хорошего тона при работе с файловыми потоками,
            // когда файл открывается с помощью FileStream он блокируется системой на доступ 
            // (небось встречали файлы, которые нельзя удалить, вот это как раз из-за этого, а еще вирусы так любят делать)
            // using -- гарантирует нам что по выполнению блока, следующего за using, поток будет закрыт и доступ к файлу освободится. 
            using (var fs = new FileStream(saveFileDialog.FileName, FileMode.OpenOrCreate))
            {
                // так как указали что хотим хранить массив, преобразуем список в массив ToArray()
                serializer.Serialize(fs, this.solarSystemObjects.ToArray()); 
            }
            
        }
    }
    
    // ...
}

запустим и проверим как оно работает, нажмем кнопку

и куда-нибудь сохраним файлик (не обязательно указывать существующий, можно просто выбрать папку и ввести название):

откроем файлик и увидим что-то в роде:

<?xml version="1.0"?>
<ArrayOfSolarSystemObject xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <SolarSystemObject>
    <Name>Земля</Name>
    <DistanceToTheSun>1.496E+08</DistanceToTheSun>
    <DayLengthInDays>1</DayLengthInDays>
    <YearLengthInDays>365</YearLengthInDays>
    <Radius>1</Radius>
  </SolarSystemObject>
  <SolarSystemObject>
    <Name>Юпитер</Name>
    <DistanceToTheSun>7.405736E+08</DistanceToTheSun>
    <DayLengthInDays>0.413</DayLengthInDays>
    <YearLengthInDays>4332.589</YearLengthInDays>
    <Radius>69911</Radius>
  </SolarSystemObject>
</ArrayOfSolarSystemObject>

вот так вот, бесплатно и без смс, получили такую сложную структуру, в файлике. Теперь давайте научимся загружать данные из файла.

Читаем xml файл

Чтение файла двойственная задача к записи, так что код получится почти такой же. И так давайте добавим новую кнопку:

кликнем на нее дважды чтобы добавить обработчик события, и впишем следующий код:

private void button3_Click(object sender, EventArgs e)
{
    // тут используем OpenFileDialog, потому что нам нужен существующий файл
    var openFileDialog = new OpenFileDialog();
    if (openFileDialog.ShowDialog() != DialogResult.OK)
        return;
    
    // тут  пока все так же
    var serializer = new XmlSerializer(typeof(SolarSystemObject[]));
    
    // тут используем FileMode.Open
    using (var fs = new FileStream(openFileDialog.FileName, FileMode.Open)) 
    {
        // тут уже похитрее, вызывая serializer.Deserialize(fs), мы считываем данные из файла,
        // но так как C# недостаточно умен он не знает, что именно мы считали, поэтому чтобы ему подсказать
        // мы используем явное указание типа путем добавления конструкции (SolarSystemObject[])
        // Почему именно такую конструкцию? 
        // Потому что выше создавая сериалайзер мы передавали параметром typeof(SolarSystemObject[])
        // Использование скобочек же — это просто синтаксис такой.
        var arrayOfSolarObjects = (SolarSystemObject[]) serializer.Deserialize(fs);
        
        // очищаем существующий список с объектами
        this.solarSystemObjects.Clear();
        
        // и последовательно по одному добавляем содержимое arrayOfSolarObjects, 
        // который содержит набор объектов считанных из файла
        foreach(var solarObject in arrayOfSolarObjects)
        {
            this.solarSystemObjects.Add(solarObject);
        }
    }
}

запускам, проверяем:

на сегодня все, о том, как сделать из этого курсовую мы узнаем в части 2.