Pull to refresh

Мое знакомство с ASP.NET MVC в Visual Studio 2015 на примере построения прототипа МИС

Reading time 30 min
Views 51K
В этом году в качестве курсовой работы мне нужно было написать несложную медицинскую информационную систему (МИС) для небольшой частной клиники по лечению эпилепсии.

База данных пациентов в клинике уже была, написана она была еще в далеком 1998 году в Microsoft Access того времени (причем даже с красивым пользовательским интерфейсом), но вот работала она только в одном месте — на компьютере заведующего, да еще и поддерживать ее стало совершенно невозможно. Значит, давно назрела необходимость внедрять что-то новое!

Сказано — сделано. Работать надо было быстро (все-таки сдавать курсовую пора) и при этом хотелось сделать работу максимально интересной для себя. Я давно хотел разобраться с ASP.NET MVC, был немного знаком с C# и общими принципами MVC, поэтому скачал последнюю Visual Studio 2015 RC и принялся за работу.

Под катом — весь процесс знакомства с технологией и разработки такой системы со всеми встретившимися подводными камнями. Данная статья пригодится всем, кто знаком с программированием и когда-то делал блог по туториалу для любого MVC фреймфорка и хочет узнать, как же сделать что то более интересное.

Итак, начнем!

Постановка задачи


Необходимо написать единую базу данных пациентов для небольшой клиники по лечению эпилепсии. После обсуждения с руководством центра были сформированы следующие требования:
  • Интеграция в систему всех накопленных данных из предыдущей базы данных;
  • Возможность доступа к системе со всех рабочих мест разных зданиях;
  • Разделение прав;
  • Наличие средства для составления расписания приемов;
  • Возможность добавления в профиль пациента фотографий, видеофайлов и документов произвольного типа;
  • Возможность редактирования встроенных словарей (диагнозы, назначения, исследования…);
  • Простота поддержки и масштабирования;
  • Возможность поиска по произвольным полям историй болезни всех пациентов.

Реализация


Отправная точка для того, чтобы понять, как работает ASP.NET MVC — официальные учебные пособия на сайте http://www.asp.net/mvc. Для моей работы на первое время достаточно было вот этого getting started. Я также попытался посмотреть учебник на Хабре, но лично мне он показался слишком специализированным.

Из пособия на сайте Microsoft (его лучше проделать руками, дабы лучше прочувствовать) отличная понятна общая механика процесса, работа контроллеров и представлений для одной модели, но не ясно, как делать более сложные приложения. Именно это я и собираюсь разобрать.

Разобравшись с основами, приступаем к работе.

Создаем новое ASP.NET приложение и запускаем его по Ctrl-F5:

Создание проекта
image

Написание моделей


Первая стадия нашей работы — описание используемых моделей. В моем случае все было просто. Необходимость быстрой интеграции старых данных Access продиктовала самое простое решение — в точности копировать старую схему данных, а уже потом, по необходимости, менять ее.



Если кратко, то есть пациент (персональные данные), связанный приемами (дата и врач). На каждом приеме врач может добавлять документы различных типов: анамнезы, диагнозы, назначения и т.д. А каждый такой анамнез, диагноз и другие документы и имеет тип из словаря — отдельной таблицы с перечислением.

Ну что ж, создаем новый файл Pacient.cs в папке Models и поочередно описываем каждую модель. Например:

Добавление нескольких моделей
public class DiagnosisType
{
    public int ID { get; set; }
    [DisplayName("Диагноз")]
    public String name { get; set; }
    [DisplayName("Описание")]
    [DataType(DataType.MultilineText)]
    public String description { get; set; }
}
public class Diagnosis
{
    public int ID { get; set; }
    [DisplayName("Диагноз")]  
    public DiagnosisType type { get; set; }
    [DisplayName("Комментарий")]
    [DataType(DataType.MultilineText)]
    public String comments { get; set; }

}
public class VisitDate
{
    public int ID { get; set; }
    public int doctorID { get; set; }
    [DisplayName("Дата приема")]
    public DateTime date { get; set; }
    public List<Anamnesis> anamnesis { get; set; }
    public List<Debut> debutes { get; set; }
    public List<Diagnosis> diagnoses { get; set; }
    public List<Research> researches { get; set; }
    public List<Assigment> assigments { get; set; }
    public List<Neurostatus> neurostatuses { get; set; }
    public List<Review> reviews { get; set; }
    public List<Syndrome> syndromes { get; set; }
}
public enum Sex
{
    [Display(Name = "Противоречивый")]
    A,
    [Display(Name = "Женский")]
    F,
    [Display(Name = "Мужской")]
    M,
    [Display(Name = "Не применимо")]
    N,
    [Display(Name = "Другой")]
    O,
    [Display(Name = "Неизвестный")]
    U
}
public class Pacient
{
    public int ID { get; set; }
    [DisplayName("Лечащий врач")]
    public Doctor doctor { get; set; }
    [DisplayName("ФИО")]
    public String name { get; set; }
    [DisplayName("Номер карты")]
    public String cart { get; set; }
    [DisplayName("Телефон")]
    [DataType(DataType.PhoneNumber)]
    [Phone]
    public String phone { get; set; }
    [DisplayName("Дата регистрации в системе")]
    [DataType(DataType.Date)]
    public DateTime dateOfregistration { get; set; }
    [DisplayName("Пол")]

    public Sex sex { get; set; }
    [DisplayName("Дата рождения")]

    [DataType(DataType.Date)]
    [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
    public DateTime birthday { get; set; }
    [DisplayName("Мать")]
    public String mother { get; set; }
    [DisplayName("Отец")]
    public String father { get; set; }
    [DisplayName("Адрес проживания")]

    public String adress { get; set; }
    [DisplayName("Коментарии")]
    [DataType(DataType.Html)]
    [AllowHtml]
    public String comments { get; set; }
    public List<VisitDate> visits { get; set; }
}


Остальные классы из модели определяются по аналогии.

Замечания:
  • [DisplayName(«Диагноз»)] — задает человеко-читаемое имя для поле, используется в представлениях;
  • [DataType(DataType.MultilineText)] — в представлении для этого поля автоматически подставится textarea;
  • [AllowHtml] — позволяет хранить в этом поле html, по-умолчанию это запрещено.


Общие идеи генерации таблиц базы данных из моделей в Entity Framework:
  • Одна модель (один класс) – одна таблица;
  • Переменные класса стандартных типов – поля таблицы в базе данных;
  • Для создания поля стандартного типа, которое может содержать NULL, к имени типа необходимо добавить знак вопроса;
  • Объект другого класса, тоже являющийся моделью приводит к созданию поля внешнего ключа, указывающего на запись в таблице, соответствующей этой модели;
  • Включение в модель списка элементов приводит к созданию связи один ко многим. В модели элементов списка добавляется внешний ключ;
  • Включение в два класса списков, содержащих объекты другого приводит к образованию связи многие-ко-многим и созданию дополнительной таблицы для этой связи.

Отлично, мы оформили все модели. Теперь необходимо сказать Entity Framework'у, что это именно модели для базы данных. Для этого создаем новый контекст соединения. Один контекст — одна база данных.

Код создания контекста
public class PacientDBContext : DbContext
{
    public DbSet<Pacient> pacients { get; set; }
    public DbSet<AnamnesisEventType> anamnesisTypes { get; set; }
    public DbSet<Anamnesis> anamneses { get; set; }
    public DbSet<Debut> debutes { get; set; }
    public DbSet<DebutType> debuteTypes { get; set; }
    public DbSet<Diagnosis> diagnoses { get; set; }
    public DbSet<DiagnosisType> diagnosisTypes { get; set; }
    public DbSet<Research> researches { get; set; }
    public DbSet<ResearchType> researchTypes { get; set; }
    public DbSet<Medicine> medicines { get; set; }
    public DbSet<MedicineType> medicineTypes { get; set; }
    public DbSet<Neurostatus> neurostatuses { get; set; }
    public DbSet<NeuroStatusType> neuroStatusTypes { get; set; }
    public DbSet<Assigment> assigments { get; set; }
    public DbSet<AssigmentType> assigmentTypes { get; set; }
    public DbSet<Syndrome> syndromes { get; set; }
    public DbSet<SyndromeType> syndromeTypes { get; set; }
    public DbSet<Review> reviews { get; set; }
    public DbSet<VisitDate> visits { get; set; }
    public DbSet<Doctor> doctors { get; set; }

}
</lang>
</spoiler>
Работа, конечно, скучная, но времени экономится море. Осталась самая малость - написать об этом контексте в Web.config в корне проекта:

<spoiler title="Код для Web.config">
<source lang="xml">
<connectionStrings>
    <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\Users.mdf;Initial Catalog=aspnet-WebApplication2-20150526031246;Integrated Security=True" providerName="System.Data.SqlClient" />
    
    <add name="PacientDBContext" connectionString="Data Source=(LocalDB)\v11.0;AttachDbFilename=|DataDirectory|\Pacients.mdf;Integrated Security=True" providerName="System.Data.SqlClient" />
  </connectionStrings>


Внимание! Подстава! По умолчанию в Default Context написано Data Source=(LocalDb)\MSLocalDB, перед развертыванием оказалось, что это SQL Server Express 2014, а вот мой хостинг о нем совсем ничего не знал! Лучше сразу поставить Express 2012 (если его нету) и исправить на v11.0.

Теперь осталось только запустить приложение и система создаст новую базу данных… Или нет? У меня это происходило только при первом запросе доступа к этим данным. Но после обращения к данным слева в Обозревателе серверов можно наблюдать созданную для нас базу данных:



Кстати, если в последствии модели надо слегка поменять или что то добавить, необходимости пересоздавать базу нету. Для этого существуют автоматические миграции. Порядок работы: открываем консоль диспетчера пакетов, включаем миграции командой Enable-Migrations –EnableAutomaticMigrations -ContextTypeName WebApplication2.Models.PacientDBContex, для обновления базы в дальнейшем даем команду update-database. Подробнее — тут.



Добавление контроллеров


Следующий шаг — добавление контроллеров для каждого типах данных в системе. Процесс автоматически-ручной. Нужно добавить контроллеры для каждого типа данных.

Процесс добавления контроллера




Отлично, теперь у нас есть контроллеры и стандартные представления для просмотра, добавления и изменения всех данных! Наверное и связывать их можно, как в админке Django… или нет?

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

Для этого в PacientsContrtoller.cs меняем метод Details:

public ActionResult Details(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    //db.
    Pacient pacient = db.pacients
        .Include(p=>p.doctor)
            .Include(p => p.visits.Select(w => w.anamnesis.Select(r=>r.type)))
            .Include(p => p.visits.Select(w => w.debutes.Select(r => r.type)))
            .Include(p => p.visits.Select(w => w.diagnoses.Select(r => r.type)))
            .Include(p => p.visits.Select(w => w.researches.Select(r => r.type)))
            .Include(p => p.visits.Select(w => w.anamnesis.Select(r => r.type)))
            .Include(p => p.visits.Select(w => w.neurostatuses.Select(r => r.type)))
            .Include(p => p.visits.Select(w => w.assigments.Select(r => r.type)))
            .Include(p => p.visits.Select(w => w.syndromes.Select(r => r.type)))
            .Include(p => p.visits.Select(w => w.reviews))
            .Where(p=>p.ID == id).Single();
    pacient.visits.Sort(delegate (VisitDate t1, VisitDate t2) { return t2.date.CompareTo(t1.date); });
    return View(pacient);
}

В этом ужасно некрасивом LINQ запросе мы просим систему подгрузить абсолютно все данные о пациентах. Для этого используются Include, а для второго уровня вложенности — Select.

Для реализации поиска по имени или по слову в резюме приема тоже используем хитрый запрос:

public ActionResult SearchByName(String name = "", String mode = "name")
{
            
    if (mode.Equals("name"))
        return PartialView(db.pacients.Where(p => p.name.Contains(name)).ToList());
    else
    {
        var results = db.pacients.Where(p => p.visits.Any(vd => vd.reviews.Any(r => r.comments.ToLower().Contains(name.ToLower()))));
        return PartialView(results.ToList());
    }
}

Напоминаю, что параметры для метода контроллера — это то, что приходит в GET запросе (в адресной строке после знака вопроса).

Остальные методы по сути остаются без изменений.

В каждом из контроллеров для документов (анамнезы, диагнозы, резюме...) я создал 4 новых метода в замен стандартных, они будут возвращать частичные (partial) представления по AJAX:

Реализация
public ActionResult pacientDetails(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Anamnesis anamnesis = db.anamneses.Include(p => p.type).Where(p => p.ID == id).First();
    if (anamnesis == null)
    {
        return HttpNotFound();
    }
    return PartialView("~/views/Anamnesis/pacientDetails.cshtml", anamnesis);
}

// GET: Anamnesis/Create
public ActionResult pacientCreate(int visitID, int num)
{
    newAnamnesis na = new newAnamnesis();
    na.visitID = visitID;
    na.num = num;
    na.anamnesis = new Anamnesis();
    na.eventTypes = db.anamnesisTypes.ToList();
    return PartialView(na);
}
public ActionResult Create(newAnamnesis data)
{
    VisitDate visit = db.visits.Include(v => v.anamnesis).Where(v => v.ID == data.visitID).First();

    if (visit == null)
        return RedirectToAction("Index", "Pacients");

    Pacient pacient = db.pacients.Where(p => p.visits.Any(v => v.ID == data.visitID)).First();
    if (pacient == null)
        return RedirectToAction("Index", "Pacients");

    if (ModelState.IsValid)
    {
        AnamnesisEventType type = db.anamnesisTypes.Where(a => a.ID == data.anamnesis.type.ID).First();
        data.anamnesis.type = type;
        visit.anamnesis.Add(data.anamnesis);       
        db.SaveChanges();
        return PartialView("/views/Anamnesis/pacientDetails.cshtml", data.anamnesis);
    }
    return PartialView("/views/Anamnesis/pacientCreate.cshtml", data);

}
// GET: Anamnesis/Edit/5
public ActionResult pacientEdit(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Anamnesis anamnesis = db.anamneses.Include(p=>p.type).Where(p=>p.ID == id).First();
            
    if (anamnesis == null)
    {
        return HttpNotFound();
    }
    return PartialView(anamnesis);
}

// POST: Anamnesis/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult pacientEdit([Bind(Include = "ID,comments")] Anamnesis anamnesis)
{
    if (ModelState.IsValid)
    {
        db.Entry(anamnesis).State = EntityState.Modified;
        db.SaveChanges();
        return pacientDetails(anamnesis.ID);
    }
    return PartialView(anamnesis);
}

// GET: Anamnesis/Delete/5
public ActionResult pacientDelete(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Anamnesis anamnesis = db.anamneses.Find(id);
    if (anamnesis == null)
    {
        return HttpNotFound();
    }
    db.anamneses.Remove(anamnesis);
    db.SaveChanges();
    return PartialView();
}


Изменение стандартных представлений


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

На главной странице будем показывать форму поиска по двум параметрам на выбор и с помощью JQuery динамически подгружать результаты.

Код страницы списка пациентов - Views/Pacient/Details.cshtml
@{
    ViewBag.Title = "Index";
}
<div class="row">
<div class="col-md-6">
        <h2>Поиск пациентов</h2>
</div>
    <div class="col-md-6">
        <a href="/Pacients/Create/" class="btn btn-success pull-right" style="margin-top: 20px; margin-right: 20px;"><span class="glyphicon glyphicon-plus"></span>Добавить</a>
    </div>
    </div>
<div>
    <form class="form-horizontal">
        <div class="input-group input-group-lg col-md-12 bs-callout bs-callout-primary">
            <label for="search" class="sr-only">Введите имя пациента</label>
            <div class="col-sm-10">
                <input type="text" placeholder="Введите имя пациента" name="name" class="col-sm-10 form-control" id="search" />
            </div>
            <div class="col-sm-2">
                <input type='button' id="submit" value='Поиск' class="btn" />
             </div>
            <div class="col-sm-10">
                <label class="radio-inline">
                    <input type="radio" name="searchOptions" id="searchByName" value="name" checked> По имени
                </label>
                <label class="radio-inline">
                    <input type="radio" name="searchOptions" id="reviewSearch" value="review"> По резюме
                </label>
            </div>

            </div>

    </form>
</div>
    <div id="results"></div>
    <script type="text/javascript">
        $(document).ready(function () {
            //$('#submit').cha
            $('#submit').click(function (e) {
                e.preventDefault();
                var name = $('#search').val();
                var mode = "name";
                if ($("#reviewSearch").prop("checked"))
                {
                    mode = "review"
                }
                name = name.replace(new RegExp(" ", 'g'), "%20");
                $('#results').load("/Pacients/SearchByName?name=" + name + "&mode="+mode);
            });
            $('#search').keypress(function (event) {
                if ($("#reviewSearch").prop("checked")) return;
                if (event.which == 13) {
                    event.preventDefault();
                }
                var name = $('#search').val();
                var mode = "name";
                if ($("#reviewSearch").prop("checked")) {
                    mode = "review"
                }
                name = name.replace(new RegExp(" ", 'g'), "%20");
                $('#results').load("/Pacients/SearchByName?name=" + name + "&mode=" + mode);
            });
        });
    </script>



Шаблоны создания нового пациента и изменения оставим без изменений. А вот страницу подробной информации о пациенте пришлось разбить сразу на несколько представлений.

Шапка страницы пациента
@model WebApplication2.Models.Pacient

@{
    ViewBag.Title = "Details";
}
<div class="row">
    <div class="col-md-2" style=" margin-top: 30px;">
        <a href="@Url.Action("Index" )" class = "btn btn-default btn-lg"><span class="glyphicon glyphicon-backward" aria-hidden="true"></span> Назад
        </a>
            
    </div>
    <div class="col-md-6"><h2>@Html.DisplayFor(model => model.name)</h2>


    <h4>@Html.DisplayNameFor(model => model.doctor): @Html.DisplayFor(model => model.doctor.name)</h4>
    
    </div>
        @if (Model.visits.Count==0 || !(Model.visits.First().date.Equals(DateTime.Today)))
        {
            <span  style=" margin-top: 30px;margin-right: 30px;" class="pull-right">
                <a href="@Url.Action("Create", "visitDates", new {id=Model.ID })" class="btn btn-default btn-primary btn-lg"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Начать прием</a>
            </span>
    }
</div>
  <hr /> 
@Html.Partial("~/Views/Pacients/visitsView.cshtml", Model)


Шапка командой Html.Partial("~/Views/Pacients/visitsView.cshtml", Model) подгружает список всех документов. В начале списка документов находятся общие JS-функции для работы динамической подгрузки данных. VS2015 по-умолчанию поддерживает AngularJS, но в данном проекте я решил обойтись без него — проще, зато понятнее. Благо для этого понадобились всего четыре процедуры.

Во вкладке приемы находятся все данные упорядоченные по дате приема, в остальных вкладка — отдельные типы документов. Для упрощения все опять же разнесено по разным представлениям.

Views/Pacients/visitsView.cshtml

@model WebApplication2.Models.Pacient
@using WebApplication2.Models

<script>
    function Delete(controller, id) {
        if (confirm("Вы действительно хотите безвозвратно это удалить?")) {
            $('#' + controller + 'Div' + id).load('/' + controller + '/pacientDelete/' + id);
            $('.' + controller + 'Div' + id).load('/' + controller + '/pacientDelete/' + id);
        }
    }
    function Cancel(controller, id) {
        if (confirm("Лекарство будет отменено, но оно останется в истории приемов. Продолжаем?")) {
            $("#" + controller + "Tab").find('#' + controller + 'Div' + id).load('/' + controller + '/pacientCancel/' + id);
            $("#" + controller + "Tab").find('.' + controller + 'Div' + id).load('/' + controller + '/pacientCancel/' + id);
        }
    }
    function CancelEdit(controller, id) {
        $.get('/' + controller + '/pacientDetails/' + id, function (data) {
            res = $.parseHTML('<div>' + data + '</div>');
            if ($(res).find('.' + controller + 'Div' + id).html() != "") {
                var content = $(res).find('.' + controller + 'Div' + id).html();
            }
            else {
                var content = $(res).find('#' + controller + 'Div' + id).html();
            }
            $('#' + controller + 'Div' + id).html(content);
            $('.' + controller + 'Div' + id).html(content);
        });
       
        
    }
    function LoadEditForm(controller, id) {
        if ($("#" + controller + "Tab").hasClass("active"))
        {
            $("#" + controller + "Tab").find('#' + controller + 'Div' + id).load('/' + controller + '/pacientEdit/' + id);
            $("#" + controller + "Tab").find('.' + controller + 'Div' + id).load('/' + controller + '/pacientEdit/' + id);
        }
        else
        {
            $("#dateTab").find('#' + controller + 'Div' + id).load('/' + controller + '/pacientEdit/' + id);
            $("#dateTab").find('.' + controller + 'Div' + id).load('/' + controller + '/pacientEdit/' + id);
        }
        
    }
    function PostEditForm(controller, id, mce) {
        if (mce == true) tinyMCE.triggerSave();
        $.ajax({
            type: "POST",
            url: '/' + controller + '/pacientEdit/' + id,
            data: $('.' + controller + 'Edit' + id).serialize() + $('#' + controller + 'Edit' + id).serialize(), // serializes the form's elements.
            success: function (data) {
                res = $.parseHTML('<div>' + data + '</div>');
                if ($(res).find('.' + controller + 'Div' + id).html() != "")
                {
                    var content = $(res).find('.' + controller + 'Div' + id).html();
                }
                else {
                    var content = $(res).find('#' + controller + 'Div' + id).html();
                }
                $('#' + controller + 'Div' + id).html(content);
                $('.' + controller + 'Div' + id).html(content);
            }
        });

    }
    function PostCreateForm(controller, num, mce) {
        if (mce == true) tinyMCE.triggerSave();
        $.ajax({
            type: "POST",
            url: '/' + controller + '/Create/',
            data: $('#' + controller + 'Create').serialize(), // serializes the form's elements.
            success: function (data) {
                res = $.parseHTML('<div><div>' + data + '</div></div>');
                $('#documentData' + num).prepend($(res).find('div').first().html());
                $('#' + controller + 'Tab').find('.tabContent').prepend($(res).find('div').first().html());
                $('#' + controller + 'Create').trigger('reset');
            }
        });

    }

</script>

<div role="tabpanel">
    <ul class="nav  nav-pills nav-justified">
        <li role="presentation" class="active"><a href="#dateTab" aria-controls="dateTab" role="tab" data-toggle="tab">Приемы</a></li>
        <li role="presentation"><a href="#InfoTab" aria-controls="InfoTab" role="tab" data-toggle="tab">Персональные данные</a></li>
        <li role="presentation"><a href="#AnamnesisTab" aria-controls="AnamnesisTab" role="tab" data-toggle="tab">Анамнез</a></li>
        <li role="presentation"><a href="#DebutsTab" aria-controls="DebutsTab" role="tab" data-toggle="tab">Дебют</a></li>
        <li role="presentation"><a href="#DiagnosesTab" aria-controls="DiagnosesTab" role="tab" data-toggle="tab">Диагнозы</a></li>
        <li role="presentation"><a href="#SyndromesTab" aria-controls="SyndromesTab" role="tab" data-toggle="tab">Приступы</a></li>
        <li role="presentation"><a href="#ResearchesTab" aria-controls="ResearchesTab" role="tab" data-toggle="tab">Исследования</a></li>
        <li role="presentation"><a href="#AssigmentsTab" aria-controls="AssigmentsTab" role="tab" data-toggle="tab">Назначения</a></li>
        <li role="presentation"><a href="#NeurostatusTab" aria-controls="NeurostatusTab" role="tab" data-toggle="tab">Невростатус</a></li>
        <li role="presentation"><a href="#ReviewsTab" aria-controls="ReviewsTab" role="tab" data-toggle="tab">Резюме</a></li>
    </ul>
    <div class="tab-content">
        <div role="tabpanel" class="tab-pane active fade in" id="dateTab">
            @if (Model.visits.Count == 0)
            {
                <div class="bs-callout bs-callout-success">
                    <p>Пациент еще не был на приеме.</p>
                </div>
            }
            @if (Model.visits.Count != 0 && Model.visits.First().date.Equals(DateTime.Today))
            {
                @Html.Partial("~/Views/Pacients/documentList.cshtml", new documentList { num = 1, add = true, visit = Model.visits.First() })
            }
            @{
                int num = 9;
            }
            @foreach (var visit in Model.visits)
            {
                if (visit.date.Equals(DateTime.Today))
                {
                    continue;
                }
                @Html.Partial("~/Views/Pacients/documentList.cshtml", new documentList { num = num, add = false, visit = visit })
                num = num + 8;

            }
        </div>
        <div role="tabpanel" class="tab-pane fade" id="InfoTab">
                @Html.Partial("~/Views/Pacients/PersonalData.cshtml", Model)
        </div>
        <div role="tabpanel" class="tab-pane fade" id="AnamnesisTab">
            @Html.Partial("~/Views/Pacients/anamnesisList.cshtml", Model)
        </div>
        <div role="tabpanel" class="tab-pane fade" id="DebutsTab">
            @Html.Partial("~/Views/Pacients/debutList.cshtml", Model)
        </div>
        <div role="tabpanel" class="tab-pane fade" id="DiagnosesTab">
            @Html.Partial("~/Views/Pacients/diagnosisList.cshtml", Model)
        </div>
        <div role="tabpanel" class="tab-pane fade" id="SyndromesTab">
            @Html.Partial("~/Views/Pacients/syndromList.cshtml", Model)
        </div>
        <div role="tabpanel" class="tab-pane fade" id="ResearchesTab">
            @Html.Partial("~/Views/Pacients/researchList.cshtml", Model)
        </div>
        <div role="tabpanel" class="tab-pane fade" id="AssigmentsTab">
            @Html.Partial("~/Views/Pacients/assigmentList.cshtml", Model)
        </div>
        <div role="tabpanel" class="tab-pane fade" id="NeurostatusTab">
            @Html.Partial("~/Views/Pacients/neurostatusList.cshtml", Model)
        </div>
        <div role="tabpanel" class="tab-pane fade" id="ReviewsTab">
            @Html.Partial("~/Views/Pacients/reviewList.cshtml", Model)
        </div>
        
    </div>
    
</div>




Для примера рассмотрим одно из представлений для вкладки, например, с анамнезами.

Views/Pacient/anamnesisList.cshtml

@model WebApplication2.Models.Pacient
@using WebApplication2.Models


<div class="bs-callout bs-callout-success">
        <h5>Сводный анамнез</h5>
        <p> </p>
    <div class="tabContent">
        @foreach (var visit in Model.visits)
        {
            foreach (var anamnes in visit.anamnesis)
            {
                @Html.Partial("~/Views/Anamnesis/pacientDetails.cshtml", anamnes)
            }
        }
        </div>
    </div>




Как вы помните, мы сделали в контроллерах документов по четыре новых метода: pacientDetails, pacientCreate, pacientEdit, pacientDelete. Надо из них ссылается представление выше. Значит нужно его создать!

Добавление нового частичного представления



Создав представление, заполняем его:

Views/Anamnesis/pacientDetails.cshtml
@model WebApplication2.Models.Anamnesis

<div class="@String.Format("AnamnesisDiv{0}", Model.ID) row">
    <div class="col-md-4"><strong>
        @Html.DisplayFor(model => model.type.name)
        </strong>
    </div>
    <div class="col-md-6"><p>
        @Html.DisplayFor(model => model.comments)
        </p>
    </div>
    <div class="col-md-2">
        <button class="btn btn-success btn-sm" onclick="LoadEditForm('Anamnesis', @Model.ID)"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
        </button>
        <button class="btn btn-danger btn-sm" onclick="Delete('Anamnesis', @Model.ID)">
            <span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
        </button>
    </div>

</div>
<hr />


Вместо обычных имен здесь используются Html.DisplayFor(model => model.type.name) для отображения имени элемента. Это и позволяет задавать имена внутри моделей (как это сделано в начале поста).

Аналогично можно сделать с представлением для изменения анамнеза:

Views/Anamnesis/pacientEdit.cshtml
@model WebApplication2.Models.Anamnesis

<form class="@String.Format("AnamnesisEdit{0}", Model.ID)">

    @Html.AntiForgeryToken()
    @Html.HiddenFor(model => model.ID)
    @Html.HiddenFor(model => model.type.ID)
    <div class="form-horizontal">
        <div class="col-md-4">
            <strong>
                @Html.DisplayFor(model => model.type.name)
            </strong>
        </div>
        <div class="col-md-6">
                @Html.EditorFor(model => model.comments, new { htmlAttributes = new { @class = "form-control", @placeholder = Html.DisplayNameFor(model => model.comments) } })
                @Html.ValidationMessageFor(model => model.comments, "", new { @class = "text-danger" })
        </div>
        <div class="col-md-2">
            <a onclick="CancelEdit('Anamnesis', @Model.ID);" class="btn btn-warning btn-sm"><span class="glyphicon glyphicon-backward" aria-hidden="true"></span></a>
            <a onclick="PostEditForm('Anamnesis', @Model.ID);" class="btn btn-primary btn-sm"><span class="glyphicon glyphicon-save" aria-hidden="true"></span></a>
        </div>
    </div> 
</form>


Замечания:
  • Html.AntiForgeryToken() — добавляет уникальный ключ пользователя
  • Html.HiddenFor(model => model.ID) — добавляет <input type=«hidden»… > для поля
  • Html.ValidationMessageFor(model => model.comments, "", new { class = «text-danger» }) — отображает сообщения валидатора
  • Html.EditorFor(model => model.comments) — отображает поле для ввода соотвественно типу поля (например, input или textarea)
  • CancelEdit и PostEditForm — это описанные нами ранее JS процедуры


А вот с созданием новых объектов сложнее: нам надо связывать его с объектом из словаря, а это другая модель и другая таблица с данными. Надо тащить в форму создания список из всех возможных вариантов. Подгрузить их несложно, а вот передать в представление нельзя — она принимает только один параметр — модель. Придется создавать новую модель…

Создаем новый файл Models\viewModels.cs и добавляем туда код нашей модели для представления. В контекст его добавлять не надо.

Код модели для добавления анамнеза
public class newAnamnesis
{
    public Anamnesis anamnesis { get; set; }
    public int visitID { get; set; }
    public int? num { get; set; }
    public List<AnamnesisEventType> eventTypes { get; set; }

}



Теперь вспомним нашу функцию pacientCreate в Controllers\AnamnesisController.cs:

pacientCreate - метод добавления нового анамнеза
// GET: Anamnesis/Create
public ActionResult pacientCreate(int visitID, int num)
{
    newAnamnesis na = new newAnamnesis();
    na.visitID = visitID;
    na.num = num;
    na.anamnesis = new Anamnesis();
    na.eventTypes = db.anamnesisTypes.ToList();
    return PartialView(na);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(newAnamnesis data)
{
    VisitDate visit = db.visits.Include(v => v.anamnesis).Where(v => v.ID == data.visitID).First();

    if (visit == null)
        return RedirectToAction("Index", "Pacients");

    Pacient pacient = db.pacients.Where(p => p.visits.Any(v => v.ID == data.visitID)).First();
    if (pacient == null)
        return RedirectToAction("Index", "Pacients");

    if (ModelState.IsValid)
    {
        AnamnesisEventType type = db.anamnesisTypes.Where(a => a.ID == data.anamnesis.type.ID).First();
        data.anamnesis.type = type;
        visit.anamnesis.Add(data.anamnesis);
                
        db.SaveChanges();
        return PartialView("/views/Anamnesis/pacientDetails.cshtml", data.anamnesis);
    }
    return PartialView("/views/Anamnesis/pacientCreate.cshtml", data);

}


Фух, кажется теперь все готово. Осталось только проделать эти операции для всех остальных типов документов. К слову по идее этом можно автоматизировать, скажем создать шаблонное представление, но так в Razor делать нельзя. Поправьте, если я не прав.

Форма создания подгружается на вкладке приемы:

Код представления documentList.cshtml для вкладки Приемы
@model WebApplication2.Models.documentList
@using WebApplication2.Models
@{ var cl = "bs-callout-primary";
    var ac = "";
}
@if (Model.add == false)
{
    cl = "bs-callout-success";
    ac = "";
}

<div class="bs-callout @cl">
            @if (Model.add == false)
            {
                <h5>Прием @Model.visit.date</h5>
            }
            else
            {
                <h5>Текущий прием</h5>
            }
            @if (Model.visit.anamnesis.Count > 0 || Model.add == true)
            {
                <div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
                    <div class="panel panel-default">
                        <div class="panel-heading" role="tab" id="@String.Format("#documentHeading{0}", Model.num)" onclick="$('@String.Format("#document{0}", Model.num)').collapse('toogle');">
                            <h4 class="panel-title">
                                <a data-toggle="collapse" data-parent="#accordion" href="@String.Format("#document{0}", Model.num)" aria-expanded="true" aria-controls="collapseOne">
                                    Анамнез<span class="pull-right"><small>@Model.visit.date</small></span>
                                </a>
                            </h4>
                        </div>
                        <div id="@String.Format("document{0}", Model.num)" class="panel-collapse collapse @ac" role="tabpanel" aria-labelledby="headingOne">
                            <div class="panel-body">
                                <div id="@String.Format("documentData{0}", Model.num)">
                                    @foreach (var anamnes in Model.visit.anamnesis)
                                    {
                                        @Html.Partial("~/Views/Anamnesis/pacientDetails.cshtml", anamnes);
                                    }
                                </div>
                                
                                @if (Model.add == true)
                                {
                                    @Html.Action("pacientCreate", "Anamnesis", new { num = Model.num, visitID = Model.visit.ID})
                                }
                                
                            </div>
                        </div>
                    </div>
                </div>
                Model.num = Model.num + 1;
            }
            @if (Model.visit.debutes.Count > 0 || Model.add == true)
            {
                <div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
                    <div class="panel panel-default">
                        <div class="panel-heading" role="tab" id="@String.Format("#documentHeading{0}", Model.num)" onclick="$('@String.Format("#document{0}", Model.num)').collapse('toogle');">
                            <h4 class="panel-title">
                                <a data-toggle="collapse" data-parent="#accordion" href="@String.Format("#document{0}", Model.num)" aria-expanded="true" aria-controls="collapseOne">
                                    Дебют<span class="pull-right"><small>@Model.visit.date</small></span>
                                </a>
                            </h4>
                        </div>
                        <div id="@String.Format("document{0}", Model.num)" class="panel-collapse collapse @ac" role="tabpanel" aria-labelledby="headingOne">
                            <div class="panel-body">
                                <div id="@String.Format("documentData{0}", Model.num)">
                                    @foreach (var debut in Model.visit.debutes)
                                    {
                                        @Html.Partial("~/Views/Debuts/pacientDetails.cshtml", debut);
                                    }
                                </div>
                                @if (Model.add == true)
                                {
                                    @Html.Action("pacientCreate", "Debuts", new { num = Model.num, visitID = Model.visit.ID })
                                }
                                
                            </div>
                        </div>
                    </div>
                </div>
                Model.num = Model.num + 1;
            }
            @if (Model.visit.diagnoses.Count > 0 || Model.add == true)
            {
                <div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
                    <div class="panel panel-default">
                        <div class="panel-heading" role="tab" id="@String.Format("#documentHeading{0}", Model.num)" onclick="$('@String.Format("#document{0}", Model.num)').collapse('toogle');">
                            <h4 class="panel-title">
                                <a data-toggle="collapse" data-parent="#accordion" href="@String.Format("#document{0}", Model.num)" aria-expanded="true" aria-controls="collapseOne">
                                    Диагноз<span class="pull-right"><small>@Model.visit.date</small></span>
                                </a>
                            </h4>
                        </div>
                        <div id="@String.Format("document{0}", Model.num)" class="panel-collapse collapse @ac" role="tabpanel" aria-labelledby="headingOne">
                            <div class="panel-body">
                                <div id="@String.Format("documentData{0}", Model.num)">
                                    @foreach (var diagnosis in Model.visit.diagnoses)
                                    {
                                        @Html.Partial("~/Views/Diagnoses/pacientDetails.cshtml", diagnosis);
                                    }
                                </div>
                                @if (Model.add == true)
                                {
                                    @Html.Action("pacientCreate", "Diagnoses", new { num = Model.num, visitID = Model.visit.ID })
                                }
                            </div>
                            
                        </div>
                    </div>
                </div>
                Model.num = Model.num + 1;
            }
            @if (Model.visit.syndromes.Count > 0 || Model.add == true)
            {
                <div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
                    <div class="panel panel-default">
                        <div class="panel-heading" role="tab" id="@String.Format("#documentHeading{0}", Model.num)" onclick="$('@String.Format("#document{0}", Model.num)').collapse('toogle');">
                            <h4 class="panel-title">
                                <a data-toggle="collapse" data-parent="#accordion" href="@String.Format("#document{0}", Model.num)" aria-expanded="true" aria-controls="collapseOne">
                                    Приступы<span class="pull-right"><small>@Model.visit.date</small></span>
                                </a>
                            </h4>
                        </div>
                        <div id="@String.Format("document{0}", Model.num)" class="panel-collapse collapse @ac" role="tabpanel" aria-labelledby="headingOne">
                            <div class="panel-body">
                                <div id="@String.Format("documentData{0}", Model.num)">
                                    @foreach (var syndrome in Model.visit.syndromes)
                                    {
                                        @Html.Partial("~/Views/Syndromes/pacientDetails.cshtml", syndrome);
                                    }
                                </div>
                                @if (Model.add == true)
                                {
                                    @Html.Action("pacientCreate", "Syndromes", new { num = Model.num, visitID = Model.visit.ID })
                                }
                            </div>
                        </div>
                    </div>
                </div>
                Model.num = Model.num + 1;
            }
            @if (Model.visit.researches.Count > 0 || Model.add == true)
            {
                <div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
                    <div class="panel panel-default">
                        <div class="panel-heading" role="tab" id="@String.Format("#documentHeading{0}", Model.num)" onclick="$('@String.Format("#document{0}", Model.num)').collapse('toogle');">
                            <h4 class="panel-title">
                                <a data-toggle="collapse" data-parent="#accordion" href="@String.Format("#document{0}", Model.num)" aria-expanded="true" aria-controls="collapseOne">
                                    Исследования<span class="pull-right"><small>@Model.visit.date</small></span>
                                </a>
                            </h4>
                        </div>
                        <div id="@String.Format("document{0}", Model.num)" class="panel-collapse collapse @ac" role="tabpanel" aria-labelledby="headingOne">
                            <div class="panel-body">
                                <div id="@String.Format("documentData{0}", Model.num)">
                                    @foreach (var research in Model.visit.researches)
                                    {
                                        @Html.Partial("~/Views/Researches/pacientDetails.cshtml", research);
                                    }
                                </div>
                                @if (Model.add == true)
                                {
                                    @Html.Action("pacientCreate", "Researches", new { num = Model.num, visitID = Model.visit.ID })
                                }
                            </div>
                        </div>
                    </div>
                </div>
                Model.num = Model.num + 1;
            }
            @if (Model.visit.assigments.Count > 0 || Model.add == true)
            {
                <div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
                    <div class="panel panel-default">
                        <div class="panel-heading" role="tab" id="@String.Format("#documentHeading{0}", Model.num)" onclick="$('@String.Format("#document{0}", Model.num)').collapse('toogle');">
                            <h4 class="panel-title">
                                <a data-toggle="collapse" data-parent="#accordion" href="@String.Format("#document{0}", Model.num)" aria-expanded="true" aria-controls="collapseOne">
                                    Назначения<span class="pull-right"><small>@Model.visit.date</small></span>
                                </a>
                            </h4>
                        </div>
                        <div id="@String.Format("document{0}", Model.num)" class="panel-collapse collapse @ac" role="tabpanel" aria-labelledby="headingOne">
                            <div class="panel-body">
                                <div id="@String.Format("documentData{0}", Model.num)">
                                    @foreach (var assigment in Model.visit.assigments)
                                    {
                                        @Html.Partial("~/Views/Assigments/pacientDetails.cshtml", assigment);
                                    }
                                </div>
                                @if (Model.add == true)
                                {
                                    @Html.Action("pacientCreate", "Assigments", new { num = Model.num, visitID = Model.visit.ID })
                                }
                            </div>
                        </div>
                    </div>
                </div>
                Model.num = Model.num + 1;
            }
            @if (Model.visit.neurostatuses.Count > 0 || Model.add == true)
            {
                <div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
                    <div class="panel panel-default">
                        <div class="panel-heading" role="tab" id="@String.Format("#documentHeading{0}", Model.num)" onclick="$('@String.Format("#document{0}", Model.num)').collapse('toogle');">
                            <h4 class="panel-title">
                                <a data-toggle="collapse" data-parent="#accordion" href="@String.Format("#document{0}", Model.num)" aria-expanded="true" aria-controls="collapseOne">
                                    Невростатус<span class="pull-right"><small>@Model.visit.date</small></span>
                                </a>
                            </h4>
                        </div>
                        <div id="@String.Format("document{0}", Model.num)" class="panel-collapse collapse @ac" role="tabpanel" aria-labelledby="headingOne">
                            <div class="panel-body">
                                <div id="@String.Format("documentData{0}", Model.num)">
                                    @foreach (var neurostatus in Model.visit.neurostatuses)
                                    {
                                        @Html.Partial("~/Views/Neurostatus/pacientDetails.cshtml", neurostatus);
                                    }
                                </div>

                                @if (Model.add == true)
                                {
                                    @Html.Action("pacientCreate", "Neurostatus", new { num = Model.num, visitID = Model.visit.ID })
                                }
                            </div>
                        </div>
                    </div>
                </div>
                Model.num = Model.num + 1;
            }

            @if (Model.visit.reviews.Count > 0 || Model.add == true)
            {
                <div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
                    <div class="panel panel-default">
                        <div class="panel-heading" role="tab" id="@String.Format("#documentHeading{0}", Model.num)" onclick="$('@String.Format("#document{0}", Model.num)').collapse('toogle');">
                            <h4 class="panel-title">
                                <a data-toggle="collapse" data-parent="#accordion" href="@String.Format("#document{0}", Model.num)" aria-expanded="true" aria-controls="collapseOne">
                                    Резюме<span class="pull-right"><small>@Model.visit.date</small></span>
                                </a>
                            </h4>
                        </div>
                        <div id="@String.Format("document{0}", Model.num)" class="panel-collapse collapse @ac" role="tabpanel" aria-labelledby="headingOne">
                            <div class="panel-body">
                                <div id="@String.Format("documentData{0}", Model.num)">
                                    @foreach (var review in Model.visit.reviews)
                                    {
                                        @Html.Partial("~/Views/Reviews/pacientDetails.cshtml", review);
                                    }
                                </div>

                                @if (Model.add == true)
                                {
                                    @Html.Action("pacientCreate", "Reviews", new { num = Model.num, visitID = Model.visit.ID })
                                }
                            </div>
                        </div>
                    </div>
                </div>
                Model.num = Model.num + 1;
            }
    <div class="row">
        @Html.ActionLink("Очистить все сведения об этом приеме", "Delete", "VisitDates", new { id = Model.visit.ID }, new { @class = "btn btn-danger btn-sm pull-right", style = "margin-top: 10px;margin-right: 15px;" })
    </div>
        </div>


Для него, кстати, также потребовалось отдельно представление.

В итоге получилось вот так:





Публикация на сервер


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

Перед загрузкой нужно отредактировать Web.Release.Config в корне проекта, вписав в него connectionString для подключения к базе данных на сервере. Инструкции, как это сделать, заботливо предоставлены Microsoft прямо в самом файле.

Экспорт базы пришлось делать вручную через SQL Server Management Studio.

С Azure все должно быть еще проще — студия сама опубликует базу данных.

Заключение


К сожалению, все тонкости в одной статье описать крайне сложно, но общие моменты я постарался осветить. Полный код проекта доступен на GitHub: https://github.com/roctbb/ICNE_EHR/.

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

В качестве источников использовался сайт asp.net и бесчисленные вопросы на stackoverflow.com.
Tags:
Hubs:
+14
Comments 8
Comments Comments 8

Articles