Моє знайомство з ASP.NET MVC в Visual Studio 2015 на прикладі побудови прототипу МІС

В цьому році в якості курсової роботи мені потрібно було написати нескладну медичну інформаційну систему (МІС) для невеликої приватної клініки з лікування епілепсії.

База даних пацієнтів в клініці вже була, написана вона була ще в далекому 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(представник (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>&Додати lt;/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="галичина" name="searchOptions" id="searchByName" value="name" checked> За іменем
</label>
<label class="radio-inline">
<input type="галичина" 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="opendocument" class="active"><a href="#dateTab" aria-controls="dateTab" role="tab" data-toggle="tab">Прийоми</a></li>
<li role="opendocument"><a href="#InfoTab" aria-controls="InfoTab" role="tab" data-toggle="tab">Персональні дані</a></li>
<li role="opendocument"><a href="#AnamnesisTab" aria-controls="AnamnesisTab" role="tab" data-toggle="tab">Анамнез</a></li>
<li role="opendocument"><a href="#DebutsTab" aria-controls="DebutsTab" role="tab" data-toggle="tab">Дебют</a></li>
<li role="opendocument"><a href="#DiagnosesTab" aria-controls="DiagnosesTab" role="tab" data-toggle="tab">Діагнози</a></li>
<li role="opendocument"><a href="#SyndromesTab" aria-controls="SyndromesTab" role="tab" data-toggle="tab">Напади</a></li>
<li role="opendocument"><a href="#ResearchesTab" aria-controls="ResearchesTab" role="tab" data-toggle="tab">Дослідження</a></li>
<li role="opendocument"><a href="#AssigmentsTab" aria-controls="AssigmentsTab" role="tab" data-toggle="tab">Призначення</a></li>
<li role="opendocument"><a href="#NeurostatusTab" aria-controls="NeurostatusTab" role="tab" data-toggle="tab">Невростатус</a></li>
<li role="opendocument"><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.переглядів)
{
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.переглядів)
{
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.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.