Jak zmienić generowanie kodu w ASP.NET MVC?

Wprowadzenie

Dodając nowy kontroler lub widok do aplikacji ASP.NET MVC, wybieramy jeden z szablonów, które Visual Studio wykorzysta do ich wygenerowania. Jest to bardzo przydatne, gdy na przykład formularz edycji zawiera wiele pól. Visual Studio wygeneruje kontrolkę dla każdej właściwości z klasy, którą ustawimy jako model widoku. Podobnie jest z generowaniem kontrolerów. Jest to bardzo fajne, ale niestety na dłuższą metę tak wygenerowany kod nie jest tym, czego potrzebujemy, i nanosimy do niego wiele zmian.

Zauważyłem, że dużo osób nie wie, że Visual Studio wykorzystuje szablony T4 (podobne do szablonów, jakich użyłem do generowania repozytoriów) do generowania widoków oraz kontrolerów. W tym wszystkim najfajniejsze jest to, że możemy te szablony dowolnie modyfikować i dostosowywać do swoich potrzeb. Wszystko po to, aby jeszcze szybciej i efektywniej dodawać nowe kontrolery oraz widoki do aplikacji.

CodeTemplates – generowanie kodu w ASP.NET MVC

W zależności od wersji Visual Studio szablony znajdują się w różnej lokalizacji. U mnie w komputerze na przykład, w którym mam zainstalowane Visual Studio 2017 Enterprise, szablony znajdują się w katalogu C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\IDE\Extensions\Microsoft\Web\Mvc\Scaffolding\Templates.

Możemy zmienić szablony w tej lokalizacji, ale takie rozwiązanie nie jest zbyt dobre. Po pierwsze będzie dotyczyło to wszystkich projektów, które robiliśmy w danym Visual Studio. Jeśli robisz tylko jeden projekt, to może to się sprawdzić, ale bardzo często rozwijamy wiele projektów naraz, a możemy chcieć mieć różne szablony dla różnych projektów. Dodatkowo nie jesteśmy w stanie łatwo współdzielić takich szablonów w zespole. Warto, by każdy programista wykorzystywał te same szablony.

Innym rozwiązaniem jest dodanie szablonów do projektu ASP.NET MVC. Gdy utworzymy katalog CodeTemplates w głównym folderze projektu i tam wrzucimy szablony, Visual Studio skorzysta właśnie z nich, a nie z szablonów z Program Files. Dzięki takiemu rozwiązaniu możemy modyfikować szablony dla poszczególnych projektów oraz przy okazji są one w repozytorium dostępne dla wszystkich osób w zespole i będą automatycznie wykorzystywane przez Visual Studio.

Szablony możemy ręcznie skopiować z katalogu Visual Studio lub skorzystać z czegoś gotowego. W nugecie znajdują się pakiety z szablonami dla poszczególnych wersji ASP.NET MVC. W przykładzie skorzystałem z pakietu Mvc5CodeTemplatesCSharp (https://www.nuget.org/packages/Mvc5CodeTemplatesCSharp/). Dla innych wersji ASP.NET MVC wystarczy po prostu zmienić numerek w nazwie pakietu.

Po zainstalowaniu pakietu pojawi się nowy katalog (CodeTemplates) w projekcie z wszystkimi szablonami:

code templates solution explorer

Pakiet ma jeden minus. Mimo że w nazwie jest CSharp, to niestety dodaje również pliki szablonów dla Visual Basic, które trzeba usunąć, gdy nam to przeszkadza.

Darmowy kurs Visual Studio

Pracując z setkami programistów, zauważyłem, że większość osób nie pracuje efektywnie w Visual Studio. W skrajnych przypadkach korzystali z kopiowania z wykorzystaniem menu Edit. Wiem, że to dziwne, ale naprawdę niektórzy tak pracują. Dlatego postanowiłem stworzyć kurs Visual Studio – aby pomóc koleżankom i kolegom w efektywniejszej pracy.

Przygotowałem 30 lekcji e-mail, w których pokażę Ci, w jaki sposób pracować efektywniej i szybciej w Visual Studio. Poznasz dodatki, bez których nie wyobrażam sobie pracy w tym IDE.

Po więcej informacji zapraszam na dedykowaną stronę kursu: Darmowy Kurs Visual Studio.

Quiz C#

Ostatnio przygotowałem również quiz C#, w którym możesz sprawdzić swoją wiedzę. Podejmiesz wyzwanie?

Modyfikacja generowania widoku dodania (Create)

Modyfikowanie szablonów zaczniemy od widoku dodania obiektu. W katalogu MvcView znajdują się szablony, których Visual Studio używa podczas dodania widoku. Szablon możemy zmienić na dwa sposoby: zmienić istniejący lub dodać nowy (na przykład skopiować Create.cs.t4 pod inną nazwą i wtedy zmodyfikować). Visual Studio w oknie dodania nowego widoku wyświetla wszystkie szablony, jakie znajdzie w katalogu MvcView. Poniżej zrzut ekranu z dodatkowym szablonem Create2:

create2 view template

Jedną z rzeczy, która denerwuje mnie w domyślnych szablonach, jest tytuł widoku. Domyślnie tytuł jest ustawiany na nazwę widoku, przez co muszę go za każdym razem po utworzeniu widoku zmieniać.

W moich aplikacjach viewmodel dla widoków posiada właściwość PageTitle, której wartość jest ustawiana w samym viewmodelu lub kontrolerze. Dzięki temu dużo łatwiej można testować logikę ustawiania tytułu za pomocą testów jednostkowych niż za pomocą dodania tej logiki w widoku. Dlatego w pierwszej kolejności zmieniam sposób ustawiania tytułów w widokach.

Poniżej widać fragmenty widoku Create utworzone przez domyślny szablon Create.cs.t4 (link do szablonu) oraz zmodyfikowany Create2.cs.t4 (link):

default create view

modified create view

Sama modyfikacja szablonu nie była skomplikowana. Wystarczyło w trzech miejscach zamienić <#= ViewName#> na Model.PageTitle. Najlepiej widać to na porównaniu tych dwóch plików:

create template diff

Jak widzisz, prosta zmiana w szablonie powoduje, że przy każdym utworzeniu nowego widoku zaoszczędzam trochę czasu na modyfikowanie wygenerowanego pliku. Mogę przebudować cały szablon, aby od razu otrzymywać to, czego potrzebuję. Nic nie stoi na przeszkodzie, by zmodyfikować strukturę generowanego htmla czy inne rzeczy w widoku.

W swoich aplikacjach na początku przygotowuję wszystkie szablony widoków i dopiero mając je gotowe, zabieram się do tworzenia samych widoków. Później wystarczy tylko dodać jakieś specyficzne rzeczy do widoku – na przykład trochę javascript, który doda dynamiczne elementy.

Szablony kontrolerów

Poza szablonami widoków możemy też modyfikować szablony kontrolerów. Dzięki przygotowaniu szablonu kontrolerów możemy później zaoszczędzić bardzo dużo czasu. Pracując z programistami, zauważyłem, że większość tworzy pusty kontroler, a następnie dodaje do niego ręcznie kod przez skopiowanie go z innego kontrolera i późniejszą jego modyfikację. Rzadko używają wbudowanych szablonów.

Jednym z powodów takiego podejścia jest to, że domyślne szablony generują kod daleki od ideału. Poniżej znajduje się kod kontrolera wygenerowany przez szablon „MVC 5 Controller with views, using Entity Framework” dla klasy Product:

public class Products1Controller : Controller
{
private DataContext db = new DataContext();
// GET: /Products1/
public ActionResult Index()
{
return View(db.Products.ToList());
}
// GET: /Products1/Details/5
public ActionResult Details(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Product product = db.Products.Find(id);
if (product == null)
{
return HttpNotFound();
}
return View(product);
}
// GET: /Products1/Create
public ActionResult Create()
{
return View();
}
// POST: /Products1/Create
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see https://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include="Id,Name,IsActive,CreatedDate,CreatedUser,UpdatedDate,UpdatedUser")] Product product)
{
if (ModelState.IsValid)
{
db.Products.Add(product);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(product);
}
// GET: /Products1/Edit/5
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Product product = db.Products.Find(id);
if (product == null)
{
return HttpNotFound();
}
return View(product);
}
// POST: /Products1/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see https://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include="Id,Name,IsActive,CreatedDate,CreatedUser,UpdatedDate,UpdatedUser")] Product product)
{
if (ModelState.IsValid)
{
db.Entry(product).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(product);
}
// GET: /Products1/Delete/5
public ActionResult Delete(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Product product = db.Products.Find(id);
if (product == null)
{
return HttpNotFound();
}
return View(product);
}
// POST: /Products1/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
Product product = db.Products.Find(id);
db.Products.Remove(product);
db.SaveChanges();
return RedirectToAction("Index");
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}
}

Jak widać, ten kod jest daleki od wszystkich dobrych praktyk. Aby pokazać Ci możliwości szablonów kontrolerów, przygotowałem prostą modyfikację. Jest ona dużo lepsza od tej domyślnej, ale nie jest jeszcze docelowym rozwiązaniem, które wykorzystuję w swoich projektach – nie chciałem za bardzo rozbudowywać przykładu, aby się nie skomplikował.

W przykładzie wykorzystałem ideę viewmodeli przekazywanych do widoku, mapowanie obiektów z wykorzystaniem automappera, wstrzykiwanie zależności. Możesz użyć tego szablonu do dalszej modyfikacji i dostosowywania do swoich potrzeb. Poniżej znajduje się kod kontrolera wygenerowany przez zmodyfikowany szablon. Prawda, że jest dużo lepiej?

public class Products2Controller : Controller
{
private Lazy<IProductLogic> _logic;
protected IProductLogic Logic
{
get
{
return _logic.Value;
}
}
private Lazy<IMapper> _mapper;
protected IMapper Mapper
{
get
{
return _mapper.Value;
}
}
public Products2Controller(Lazy<IProductLogic> logic,
Lazy<IMapper> mapper)
{
this._logic = logic;
this._mapper = mapper;
}
// GET: /Products2/
public ActionResult Index()
{
var items = this.Logic.GetAllActive();
var viewModel = new IndexViewModel();
viewModel.Items = this.Mapper.Map<List<IndexItemViewModel>>(items);
return View(viewModel);
}
// GET: /Products2/Details/5
public ActionResult Details(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var model = this.Logic.GetById(id.Value);
if (model == null)
{
return HttpNotFound();
}
var viewModel = this.Mapper.Map<ProductViewModel>(model);
return View(viewModel);
}
// GET: /Products2/Create
public ActionResult Create()
{
return View();
}
// POST: /Products2/Create
// To protect from overposting attacks, please enable the specific properties you want to bind to,
// more details see https://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(ProductViewModel viewModel)
{
if (ModelState.IsValid)
{
var model = this.Mapper.Map<Product>(viewModel);
this.Logic.Add(model);
return RedirectToAction("Index");
}
return View(viewModel);
}
// GET: /Products2/Edit/5
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var model = this.Logic.GetById(id.Value);
if (model == null)
{
return HttpNotFound();
}
var viewModel = this.Mapper.Map<ProductViewModel>(model);
return View(viewModel);
}
// POST: /Products2/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to,
// more details see https://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(ProductViewModel viewModel)
{
if (ModelState.IsValid)
{
var model = this.Logic.GetById(viewModel.Id);
if (model == null)
{
return HttpNotFound();
}
this.Mapper.Map(viewModel, model);
this.Logic.Update(model);
return RedirectToAction("Index");
}
return View(viewModel);
}
// GET: /Products2/Delete/5
public ActionResult Delete(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var model = this.Logic.GetById(id.Value);
if (model == null)
{
return HttpNotFound();
}
var viewModel = this.Mapper.Map<ProductViewModel>(model);
return View(viewModel);
}
// POST: /Products2/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
this.Logic.Delete(id);
return RedirectToAction("Index");
}
}

Nie będę już opisywał zmian w szablonie. Jest ich na tyle dużo, że byłoby to prawdopodobnie nudne. Zachęcam do przejrzenia zmian szablonu na githubie (https://github.com/danielplawgo/T4Repositories/commit/8a5ba567dee4614bf399793011aa4c4ef39be3b1#diff-c781b66a7951fb83054dc5d253c37285).

Wykorzystanie zmodyfikowanego szablonu kontrolera

Chciałbym jeszcze wspomnieć o kilku założeniach, które wykorzystałem podczas modyfikowania szablonu. Szablon wykorzystuje domyślne szablony widoków do tworzenia widoków. Dlatego trzeba je zmodyfikować, a nie dodać nowe. W przykładzie zmodyfikowałem kod szablonów, aby nie generowały pól dla właściwości PageTitle, która jest w viewmodelu – aby nie trzeba było jej usuwać.

Kontroler oraz widoki są generowane dla viewmodelu (ProductViewModel), a nie modelu. Jest to spowodowane tym, że viewmodel jest modelem dla widoków. W kodzie szablonu natomiast z nazwy viewmodelu usuwam „ViewModel” i mam nazwę modelu, którą wykorzystuje w kodzie kontrolera. Aby wszystko działało, potrzebuję, by klasy takie jak logika biznesowa miały schematyczne nazwy (np. IProductLogic). Podobnie jest z metodami wykorzystywanymi w kontrolerze z logiki biznesowej (tutaj przydaje się bazowy interfejs dla logiki).

Gdy mam już taki szablon, późniejsza praca jest dużo łatwiejsza. Przed utworzeniem kontrolera tworzy on wymagane viewmodele (viewmodel dla edytowanego obiektu np. ProductViewModel oraz dla akcji index IndexViewModel). Później generuję kontroler oraz widoki z wykorzystaniem szablonu i na koniec nanoszę ewentualnie jakieś modyfikacje (na ogół jest ich mało). Do tego dochodzą jakieś inne rzeczy, jak na przykład mapowanie automappera.

Jeszcze raz zachęcam do przejrzenia szczegółowo przykładu. Jeśli będziesz miał jakieś pytania, pisz śmiało w komentarzu.

Przykład

Kod przykładu znajduje się na githubie (https://github.com/danielplawgo/T4Repositories). Jest to ten sam przykład, którego użyłem we wpisie odnośnie do generowania klas repozytoriów. Dodałem nowy projekt aplikacji ASP.NET MVC, w której są omawiane szablony. Aby uruchomić samą aplikację, wystarczy w web.config ustawić connection stringa do bazy. Testuj, sprawdzaj, modyfikuj. 🙂

Podsumowanie

Kolejny raz szablony T4 ułatwiają dodawanie kodu do aplikacji. Dzięki CodeTemplate w ASP.NET MVC możemy zmodyfikować domyślne szablony, aby generowane widoki oraz kontrolery były takie, jakich potrzebujemy. Zachęcam do ich wykorzystywania. W moich projektach bardzo się sprawdziły. Wystarczyło na początku poświęcić trochę czasu, aby później z dużą nawiązką go odzyskać podczas dodawania kolejnych kontrolerów do aplikacji.

A Ty modyfikowałeś kiedyś domyślne szablony? Czy może w ogóle ich nie wykorzystujesz i dodajesz wszystko ręcznie? Daj znać w komentarzu.

1 thought on “Jak zmienić generowanie kodu w ASP.NET MVC?

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.