Listing Movies in Person Dialog

To show list of movies a person acted in, we’ll add a tab to PersonDialog.

By default all entity dialogs (ones we used so far, which derive from EntityDialog) uses EntityDialog template at MovieTutorial.Web/Views/Templates/EntityDialog.Template.html:

  1. <div class="s-DialogContent">
  2. <div id="~_Toolbar" class="s-DialogToolbar">
  3. </div>
  4. <div class="s-Form">
  5. <form id="~_Form" action="">
  6. <div class="fieldset ui-widget ui-widget-content ui-corner-all">
  7. <div id="~_PropertyGrid"></div>
  8. <div class="clear"></div>
  9. </div>
  10. </form>
  11. </div>
  12. </div>

This template contains a toolbar placeholder (~_Toolbar), form (~_Form) and PropertyGrid (*~_PropertyGrid).

~_ is a special prefix that is replaced with a unique dialog ID at runtime. This ensures that objects in two instances of a dialog won’t have the same ID values.

EntityDialog template is shared by all dialogs, so we are not gonna modify it to add a tab to PersonDialog.

Defining a Tabbed Template for PersonDialog

Create a new file, MovieDB.PersonDialog.Template.html under Modules/MovieDB/Person/ folder with contents:

  1. <div id="~_Tabs" class="s-DialogContent">
  2. <ul>
  3. <li><a href="#~_TabInfo"><span>Person</span></a></li>
  4. <li><a href="#~_TabMovies"><span>Movies</span></a></li>
  5. </ul>
  6. <div id="~_TabInfo" class="tab-pane s-TabInfo">
  7. <div id="~_Toolbar" class="s-DialogToolbar">
  8. </div>
  9. <div class="s-Form">
  10. <form id="~_Form" action="">
  11. <div class="fieldset ui-widget ui-widget-content ui-corner-all">
  12. <div id="~_PropertyGrid"></div>
  13. <div class="clear"></div>
  14. </div>
  15. </form>
  16. </div>
  17. </div>
  18. <div id="~_TabMovies" class="tab-pane s-TabMovies">
  19. <div id="~_MoviesGrid">
  20. </div>
  21. </div>
  22. </div>

The syntax we used here is specific to jQuery UI tabs widget. It needs an UL element with list of tab links pointing to tab pane divs (.tab-pane).

When EntityDialog finds a div with ID ~_Tabs in its template, it automatically initializes a tabs widget on it.

Naming of the template file is important. It must end with .Template.html extension. All files with this extension are made available at client side through a dynamic script.

Folder of the template file is ignored, but templates must be under Modules or Views/Template directories.

By default, all templated widgets (EntityDialog also derives from TemplatedWidget class), looks for a template with their own classname. Thus, PersonDialog looks for a template with the name MovieDB.PersonDialog.Template.html, followed by PersonDialog.Template.html.

MovieDB comes from PersonDialog namespace with the root namespace (MovieTutorial) removed. You can also think of it as module name dot class name.

If a template with class name is not found, search continues to base classes and eventually a fallback template, EntityDialog.Template.html is used.

Now, we have a tab in PersonDialog:

Person With Tabs Initial

Meanwhile, i noticed Person link is still under MovieDB and we forgot to remove MovieCast link. I’m fixing them now…

Creating PersonMovieGrid

Movie tab is empty for now. We need to define a grid with suitable columns and place it in that tab.

First, declare the columns we’ll use with the grid, in file PersonMovieColumns.cs next to PersonColumns.cs:

  1. namespace MovieTutorial.MovieDB.Columns
  2. {
  3. using Serenity.ComponentModel;
  4. using System;
  5. [ColumnsScript("MovieDB.PersonMovie")]
  6. [BasedOnRow(typeof(Entities.MovieCastRow))]
  7. public class PersonMovieColumns
  8. {
  9. [Width(220)]
  10. public String MovieTitle { get; set; }
  11. [Width(100)]
  12. public Int32 MovieYear { get; set; }
  13. [Width(200)]
  14. public String Character { get; set; }
  15. }
  16. }

Next define a PersonMovieGrid class, in file PersonMovieGrid.ts next to PersonGrid.ts:

  1. namespace MovieTutorial.MovieDB {
  2. @Serenity.Decorators.registerClass()
  3. export class PersonMovieGrid extends Serenity.EntityGrid<MovieCastRow, any>
  4. {
  5. protected getColumnsKey() { return "MovieDB.PersonMovie"; }
  6. protected getIdProperty() { return MovieCastRow.idProperty; }
  7. protected getLocalTextPrefix() { return MovieCastRow.localTextPrefix; }
  8. protected getService() { return MovieCastService.baseUrl; }
  9. constructor(container: JQuery) {
  10. super(container);
  11. }
  12. }
  13. }

We’ll actually use MovieCast service, to list movies a person acted in.

Last step is to instantiate this grid in PersonDialog.ts:

  1. @Serenity.Decorators.registerClass()
  2. @Serenity.Decorators.responsive()
  3. export class PersonDialog extends Serenity.EntityDialog<PersonRow, any> {
  4. protected getFormKey() { return PersonForm.formKey; }
  5. protected getIdProperty() { return PersonRow.idProperty; }
  6. protected getLocalTextPrefix() { return PersonRow.localTextPrefix; }
  7. protected getNameProperty() { return PersonRow.nameProperty; }
  8. protected getService() { return PersonService.baseUrl; }
  9. protected form = new PersonForm(this.idPrefix);
  10. private moviesGrid: PersonMovieGrid;
  11. constructor() {
  12. super();
  13. this.moviesGrid = new PersonMovieGrid(this.byId("MoviesGrid"));
  14. this.tabs.on('tabsactivate', (e, i) => {
  15. this.arrange();
  16. });
  17. }
  18. }

Remember that in our template we had a div with id ~_MoviesGrid under movies tab pane. We created PersonMovie grid on that div.

this.ById(“MoviesGrid”) is a special method for templated widgets. $(‘#MoviesGrid’) wouldn’t work here, as that div actually has some ID like PersonDialog17_MoviesGrid. ~_ in templates are replaced with a unique container widget ID.

We also attached to OnActivate event of jQuery UI tabs, and called Arrange method of the dialog. This is to solve a problem with SlickGrid, when it is initially created in invisible tab. Arrange triggers relayout for SlickGrid to solve this problem.

OK, now we can see list of movies in Movies tab, but something is strange:

Person With Movies Unfiltered

Filtering Movies for the Person

No, Carrie-Anne Moss didn’t act in three roles. This grid is showing all movie cast records for now, as we didn’t tell what filter it should apply yet.

PersonMovieGrid should know the person it shows the movie cast records for. So, we add a PersonID property to this grid. This PersonID should be passed somehow to list service for filtering.

  1. namespace MovieTutorial.MovieDB
  2. {
  3. @Serenity.Decorators.registerClass()
  4. export class PersonMovieGrid extends Serenity.EntityGrid<MovieCastRow, any>
  5. {
  6. protected getColumnsKey() { return "MovieDB.PersonMovie"; }
  7. protected getIdProperty() { return MovieCastRow.idProperty; }
  8. protected getLocalTextPrefix() { return MovieCastRow.localTextPrefix; }
  9. protected getService() { return MovieCastService.baseUrl; }
  10. constructor(container: JQuery) {
  11. super(container);
  12. }
  13. protected getButtons() {
  14. return null;
  15. }
  16. protected getInitialTitle() {
  17. return null;
  18. }
  19. protected usePager() {
  20. return false;
  21. }
  22. protected getGridCanLoad() {
  23. return this.personID != null;
  24. }
  25. private _personID: number;
  26. get personID() {
  27. return this._personID;
  28. }
  29. set personID(value: number) {
  30. if (this._personID != value) {
  31. this._personID = value;
  32. this.setEquality(MovieCastRow.Fields.PersonId, value);
  33. this.refresh();
  34. }
  35. }
  36. }
  37. }

We are using ES5 (EcmaScript 5) property (get/set) features. It’s pretty similar to C# properties.

We store the person ID in a private variable. When it changes, we also set a equality filter for PersonId field using SetEquality method (which will be sent to list service),
and refresh to see changes.

Equality filter is the list request parameter that is also used by quick filter items.

Overriding GetGridCanLoad method allows us to control when grid can call list service. If we didn’t override it, while creating a new Person, grid would load all movie cast records, as there is not a PersonID yet (it is null).

List handler ignores an equality filter parameter if its value is null. Just like when a quick filter dropdown is empty, all records are shown.

We also did three cosmetic changes, by overriding three methods, first to remove all buttons from toolbar (getButtons), second to remove title from the grid (getInitialTitle) as tab title is enough), and third to remove paging functionality (usePager), a person can’t have a million movies right?).

Setting PersonID of PersonMovieGrid in PersonDialog

If nobody sets grid’s PersonID property, it will always be null, and no records will be loaded. We should set it in afterLoadEntity method of Person dialog:

  1. namespace MovieTutorial.MovieDB
  2. {
  3. // ...
  4. export class PersonDialog extends Serenity.EntityDialog<PersonRow>
  5. {
  6. // ...
  7. protected afterLoadEntity()
  8. {
  9. super.afterLoadEntity();
  10. this.moviesGrid.personID = this.entityId;
  11. }
  12. }
  13. }

afterLoadEntity is called after an entity or a new entity is loaded into dialog.

Please note that entity is loaded in a later phase, so it won’t be available in dialog constructor.

this.EntityId refers to the identity value of the currently loaded entity. In new record mode, it is null.

AfterLoadEntity and LoadEntity might be called several times during dialog lifetime, so avoid creating some child objects in these events, otherwise you will have multiple instances of created objects. Thats why we created the grid in dialog constructor.

Person With Movies Filtered

Fixing Movies Tab Size

You might have noticed that when you switch to Movies tab, dialog gets a bit less in height. This is because dialog is set to auto height and grids are 200px by default. When you switch to movies tab, form gets hidden, so dialog adjusts to movies grid height.

Edit s-MovieDB-PersonDialog css in site.less:

  1. .s-MovieDB-PersonDialog {
  2. > .size { width: 650px; }
  3. .caption { width: 150px; }
  4. .s-PersonMovieGrid > .grid-container { height: 287px; }
  5. }