Click here to Skip to main content
14,391,900 members

16 Days: A TypeScript Application from Concept to Implementation

Rate this:
5.00 (10 votes)
Please Sign up or sign in to vote.
5.00 (10 votes)
3 Nov 2019CPOL
A metadata driven, view defines the model, schema generated on the fly, from concept to prototype application in 16 days

Image 1

This screenshot is only a subset of what's been implemented here. On the right are additional links associated with projects and tasks:

Image 2

Table Of Contents

Introduction

I thought it would be fun and hopefully interesting to document the creation of a client-side TypeScript application from concept to implementation. So I chose something that I've been wanting to do for a while - a project-task manager that is tailored to my very specific requirements. But I also wanted this implementation to be highly abstract, which means metadata for the UI layout and parent-child entity relationships. In other words, at the end of the day, the physical index.html page looks like this (a snippet):

<div class="row col1">
  <div class="entitySeparator">
    <button type="button" id="createProject" class="createButton">Create Project</button>
    <div id="projectTemplateContainer" class="templateContainer"></div>
  </div>
</div>
<div class="row col2">
  <div class="entitySeparator">
    <button type="button" id="createProjectContact" class="createButton">Create Contact</button>
    <div id="projectContactTemplateContainer" class="templateContainer"></div>
  </div>
    <div class="entitySeparator">
    <button type="button" id="createProjectNote" class="createButton">
    Create Project Note</button>
    <div id="projectNoteTemplateContainer" class="templateContainer"></div>
  </div>
    <div class="entitySeparator">
    <button type="button" id="createProjectLink" class="createButton">Create Link</button>
    <div id="projectLinkTemplateContainer" class="templateContainer"></div>
  </div>
</div>

Where the real work is done on the client-side in creating the container content. So what this article covers is one way to go about creating such an implementation as a general purpose parent-child entity editor but in the context of a specific project-task manager, and you get to see the evolution from concept to working application that took place over 15 "days."

And as you've probably come to expect, I explore a couple new concepts as well:

  1. Except for concrete models, there entire "model" concept is thrown out the window. The view defines the model!
  2. Table and column generation as needed on the fly. Yup.

By the way, days are not contiguous -- while each day documents the work I did, it does not mean that I worked on this each and every day. Particularly Day 12 encompasses a physical timespan of 3 days (and no, not 8 hour days!) Also, you should realize that each day includes updating the article itself!

Also, yes, this could be implemented with grids but the default HTML grid functionality is atrocious and I didn't want to bring in other third party libraries for these articles. My favorite is jqWidgets, pretty much none other will do (even though it's large) so maybe at some point, I'll demonstrate how to tie all this stuff in to their library.

Day 1 - General Concepts

Some rough sketches:

Image 3

Image 4

  • It becomes clear looking at the layout that it is really more of a template for my own requirements and that the actual "task item bar" on the left as well as the specific fields for each task item should be completely user definable in terms of label, content, and control.
  • This pretty much means that we're looking at a NoSQL database structure with loose linkages between tasks and task items. The "root" of everything still remains the task, but the task items and their fields is really quite arbitrary.
  • So we need to be able to define the structure and its field content as one "database" of how the user wants to organize the information.
  • Certain fields end up being arrays (like URL links) that are represented discretely, while other fields (like notes) may be shown discretely as a scrollable collection of distinct textarea entries or more as a "document" where the user simply scrolls through a single textarea.
  • Searching - the user should be able to search on any field or a specific area, such as "notes."
  • Any field can be either a singleton (like a date/time on a communication) or a collection, like a list of contacts for that communication.
  • So what we end up doing first is defining an arbitrary schema with enough metadata to describe the layout and controls of the fields in the schema as well as actions on schema elements, for example, the task item bar can be represented as schema elements but they are buttons, not user input controls.
  • We don't want to go overboard with this! The complexity with this approach is that the page is not static -- the entire layout has to be generated from the metadata, so the question is, server-side generation or client-side?
  • Personally, I prefer client-side. The server should be minimally involved with layout -- the server should serve content, as in data, not layout. This approach also facilitates development of the UI without needing a server and keeps all the UI code on the client rather than spreading it across both JavaScript and C# on the back-end. And no, I'm not interested in using node.js on the back-end.

Day 2 - Some Structure Concepts for the Metadata

We should be able to have fairly simple structures. Let's define a few, all of which are of course customizable but we'll define some useful defaults.

Status

I like to have a fairly specific status and get frustrated when I can't put that information in a simple dropdown that lets me see at a glance what's going on with the task. So, I like things like:

  • To do
  • Working on
  • Testing
  • QA
  • Production (Completed)
  • Waiting for 3rd Party
  • Waiting for Coworker
  • Waiting on Management
  • Stuck

Notice that I don't have a priority next to the task. I really don't give a sh*t about priorities -- there's usually a lot of things going on and I work on what I'm in the mood for and what I can work on. Of course, if you like priorities, you can add them to the UI.

Notice that I also don't categorize tasks into, for example, sprints, platforms, customers, etc. Again, if you want those things, you can add them.

What I do want is:

  1. What is the task?
  2. What is its state?
  3. One line description of why it's in that state.

So this is what I want to see (of course, what you want to see is going to be different):

Image 5

How would we define this layout in JSON so that you can create whatever needs your needs? Pretty much, this means figuring out how to meet my needs first!

This might be the definition of the high level task list:

[
  {
    Item:
    {
      Field: "Task",
      Line: 0,
      Width: "80%"
    }
  },
  {
    Item:
    {
      Field: "Status",
      SelectFrom: "StatusList",
      OrderBy: "StatusOrder",
      Line: 0,
      Width: "20%"
    }
  },
  {
    Item:
    {
      Field: "Why",
      Line: 1,
      Width: "100%"
    }
  }
]

These fields are all inline editable but we also want to support drilling into a field to view its sub-records. Not all fields have sub-records (like Status), but this is determined by the metadata structure, so Status could have sub-records. Any time the user focuses on a control with sub-structures, the button bar will update and the "show on select" sub-structures will display the sub-records.

So we can define sub-structures, or allowable child records, like this using the Task entity as an example:

[
  {Entity:"Contact", Label:"Contacts"},
  {Entity:"Link", Label:"Links", "ShowOnParentSelect": true},
  {Entity:"KeyPoint", Label: "Key Points"},
  {Entity:"Note" Label: "Notes", "ShowOnParentSelect": true},
  {Entity:"Communication", Label: "Communications"}
]

Note that all sub-structures are defined in their singular form and we have complete flexibility as to the label used to represent the link. These "show on parent select" will always be visible unless the user collapses that section, and they are rendered in the order they appear in the list above. Where they render is determined by other layout information.

Other things to think about:

  • Sub-tasks (easy to do)
  • Task dependencies

Day 3 - Templates

So, the more I think about this, the more I realize that this is really a very generalized entity creator/editor with not quite dynamic relationships, much as I've written about in my Relationship Oriented Programming articles. So it seems natural that allowable relationships should be definable as well. But what I'd prefer to do at this point is some prototyping to get a sense of how some of these ideas can come to fruition. So let's start with the JSON above and write a function that turns it into an HTML template that can then be repeatedly applied as necessary. And at the same time, I'll be learning the nuances of TypeScript!

With some coding, I get this:

Image 6

Defined by the template array:

let template = [ // Task Template
  {
    field: "Task",
    line: 0,
    width: "80%",
    control: "textbox",
  },
  { 
    field: "Status",
    selectFrom: "StatusList",
    orderBy: "StatusOrder",
    line: 0,
    width: "20%",
    control: "combobox",
  },
  {
    field: "Why",
    line: 1,
    width: "100%",
    control: "textbox",
  }
];

and the support of interfaces to define the template object model and a Builder class to put together the HTML:

interface Item {
  field: string;
  line: number;
  width: string;
  control: string;
  selectedFrom?: string;
  orderBy?: string;
}

interface Items extends Array<Item> { }

class Builder {
  html: string;

  constructor() {
    this.html = "";
  }

  public DivBegin(item: Item): Builder {
    this.html += "<div style='float:left; width:" + item.width + "'>";

    return this;
  }

  public DivEnd(): Builder {
    this.html += "</div>";

    return this;
  }

  public DivClear(): Builder {
    this.html += "<div style='clear:both'></div>";

    return this;
  }

  public TextInput(item: Item): Builder {
    let placeholder = item.field;
    this.html += "<input type='text' placeholder='" + placeholder + "' style='width:100%'>";

    return this;
  }

  public Combobox(item: Item): Builder {
    this.SelectBegin().Option("A").Option("B").Option("C").SelectEnd();

    return this;
  }

  public SelectBegin(): Builder {
    this.html += "<select style='width:100%; height:21px'>";

    return this;
  }

  public SelectEnd(): Builder {
    this.html += "</select>";

    return this;
  }

  public Option(text: string, value?: string): Builder {
    this.html += "<option value='" + value + "'>" + text + "</option>";

    return this;
  }
}

This leaves only the logic for constructing the template:

private CreateHtmlTemplate(template: Items) : string {
  let builder = new Builder();
  let line = -1;
  let firstLine = true;

  template.forEach(item => {
    if (item.line != line) {
      line = item.line;

      if (!firstLine) {
        builder.DivClear();
      }

      firstLine = false;
    }

    builder.DivBegin(item);

    switch (item.control) {
      case "textbox":
        builder.TextInput(item);
        break;

      case "combobox":
        builder.Combobox(item);
        break;
      }

    builder.DivEnd();
  });

  builder.DivClear();

  return builder.html;
}

So the top-level code just does this:

let html = this.CreateHtmlTemplate(template);
jQuery("#template").html(html);

If I chain the template:

jQuery("#template").html(html + html + html);

I get:

Image 7

Cool. May not be the prettiest thing, but the basics are what I'm looking for.

Now personally what bugs me to no end is that the template object reminds me of ExtJs: basically a collection of arbitrary keys to define the layout of the UI. Maybe it's unavoidable, and I certainly am not going down the route that ExtJs uses which is to create custom IDs that change every time the page is refreshed. Talk about killing the ability to do test automation at the UI level. It is ironic though, in writing something like this, I begin to actually have a better understanding of the design decisions that ExtJs made.

Which brings us to how the comboboxes are actually populated. So yeah, there's a concept of a "store" in ExtJs, and manipulating the store automatically (or that's the theory) updates the UI. That's too much for me right now, but I do want the ability to use an existing object or fetch (and potentially cache) the object from a REST call. So let's put something simple together. Here's my states:

let taskStates = [
  { text: 'TODO'},
  { text: 'Working On' },
  { text: 'Testing' },
  { text: 'QA' },
  { text: 'Done' },
  { text: 'On Production' },
  { text: 'Waiting on 3rd Party' },
  { text: 'Waiting on Coworker' },
  { text: 'Waiting on Management' },
  { text: 'Stuck' },
];

With a little refactoring:

export interface Item {
  field: string;
  line: number;
  width: string;
  control: string;
  storeName?: string;  // <== this got changed to "storeName"
  orderBy?: string;
}

and the prototype concept of a store:

interface KeyStoreMap {
  [key: string] : any;  // Eventually "any" will be replaced with a more formal structure.
}

export class Store {
  stores: KeyStoreMap = {};

  public AddLocalStore(key: string, store: any) {
  this.stores[key] = store;
  }

  // Eventually will support local stores, REST calls, caching, 
 // computational stores, and using other 
  // existing objects as stores.
  public GetStore(key: string) {
    return this.stores[key];
  }
}

I now do this:

let store = new Store();
store.AddLocalStore("StatusList", taskStates);
let html = this.CreateHtmlTemplate(template, store);

and the template builder does this:

public Combobox(item: Item, store: Store) : TemplateBuilder {
  this.SelectBegin();

  store.GetStore(item.storeName).forEach(kv => {
    this.Option(kv.text);
  });

  this.SelectEnd();

  return this;
}

Resulting in:

Image 8

That was easy enough.

So what's involved with persisting the actual task data and restoring it? Seems like the store concept can be extended to save state, and one of the states I want to support is localStorage. This also seems complicated as I'm already dealing with an array of objects! And again, I realize why in ExtJS stores are always arrays of things, even if the store represents a singleton -- because it's easier! So let's refactor the Store class. First, we want something that defines the store types, like this:

export enum StoreType {
  Undefined,
  InMemory,
  LocalStorage,
  RestCall,
}

And then, we want something that manages the configuration of the store:

import { StoreType } from "../enums/StoreType"

export class StoreConfiguration {
  storeType: StoreType;
  cached: boolean;
  data: any;

  constructor() {
    this.storeType = StoreType.Undefined;
    this.data = [];
  }
}

And finally, we'll refactor the Store class so it looks like this:

import { StoreConfiguration } from "./StoreConfiguration"
import { StoreType } from "../enums/StoreType"
import { KeyStoreMap } from "../interfaces/KeyStoreMap"

export class Store {
  stores: KeyStoreMap = {};

  public CreateStore(key: string, type: StoreType) {
    this.stores[key] = new StoreConfiguration();
  }

  public AddInMemoryStore(key: string, data: object[]) {
    let store = new StoreConfiguration();
    store.storeType = StoreType.InMemory;
    store.data = data;
    this.stores[key] = store;
  }

  // Eventually will support local stores, REST calls, caching, 
 // computational stores, and using other 
  // existing objects as stores.
  public GetStoreData(key: string) {
    return this.stores[key].data;
  }
}

which is used like this:

let store = new Store();
store.AddInMemoryStore("StatusList", taskStates);
store.CreateStore("Tasks", StoreType.LocalStorage);

Next, the template that we created earlier:

let html = this.CreateHtmlTemplate(template, store);

Needs to know what store to use for the template items, so we do this instead:

let html = this.CreateHtmlTemplate(template, store, "Tasks");

Frankly, I have no idea whether this is a good idea or not, but let's go for it for now and see how it holds up.

Next we need to refactor this code jQuery("#template").html(html + html + html); so that we're not blindly copying the HTML template but instead we have a way of building the template so that it knows what object index in the store's data to update when the field changes. Dealing with decoupling sorting from the store's representation of the data will be an interesting thing to figure out. Later. More to the point, that particular line of code will probably be tossed completely when we implement loading the tasks from localStorage. For the moment, in the template builder, let's add a custom attribute storeIdx to our two controls:

this.html += "<input type='text' placeholder='" + placeholder + "' 
             style='width:100%' storeIdx='{idx}'>";

and:

this.html += "<select style='width:100%; height:21px' storeIdx='{idx}'>";

And now we do this:

let html = this.CreateHtmlTemplate(template, store, "Tasks");
let task1 = this.SetStoreIndex(html, 0);
let task2 = this.SetStoreIndex(html, 1);
let task3 = this.SetStoreIndex(html, 2);
jQuery("#template").html(task1 + task2 + task3);

with a little help from:

private SetStoreIndex(html: string, idx: number) : string {
  // a "replace all" function.
  let newHtml = html.split("{idx}").join(idx.toString());

  return newHtml;
}

and lo-and-behold, we have indices now to the store, for example:

Image 9

Sigh. Note that the resulting HTML has the storeIdx attribute as all lowercase. This seems to be a jQuery thing that I'll investigate later. Next, we need to create onchange handlers for updating the store when the value changes. This must be done with "late binding" because the HTML is created dynamically from a template. Again I see why ExtJS ends up assigning arbitrary ID's to elements -- how do we identify the element to which to bind the onchange handler? Personally, I prefer using a separate attribute to uniquely identify the binding point, and probably a GUID for the attribute value. Who knows what that will do to performance if there's hundreds of elements that must be bound, but honestly, I'm not going to worry about that!

It's 10:30 PM, I'm calling it a night!

Day 4 - Late Binding

So here, we are with the task of implementing late binding. First, a couple refactorings to the template builder to set up the bindGuid attribute with a unique identifier which we'll use to determine the binding, again using the input and select elements as examples:

public TextInput(item: Item, entityStore: StoreConfiguration) : TemplateBuilder {
  let placeholder = item.field;
  let guid = Guid.NewGuid();
  this.html += "<input type='text' placeholder='" + placeholder + 
              "' style='width:100%' storeIdx='{idx}' bindGuid='" + guid.ToString() + "'>";
  let el = new TemplateElement(item, guid);
  this.elements.push(el);

 return this;
}

public SelectBegin(item: Item) : TemplateBuilder {
  let guid = Guid.NewGuid();
  this.html += "<select style='width:100%; height:21px' 
               storeIdx='{idx}' bindGuid='" + guid.ToString() + "'>";
  let el = new TemplateElement(item, guid);
  this.elements.push(el);

  return this;
}

These all get put into an array:

elements: TemplateElement[] = [];

which the binding process on the document being ready wires up:

jQuery(document).ready(() => {
  // Bind the onchange events.
  builder.elements.forEach(el => {
    let jels = jQuery("[bindGuid = '" + el.guid.ToString() + "']");

    jels.each((_, elx) => {
      let jel = jQuery(elx);

      jel.on('change', () => {
        let recIdx = jel.attr("storeIdx");
        console.log("change for " + el.guid.ToString() + " at index " + 
                    recIdx + " value of " + jel.val());
        taskStore.SetProperty(Number(recIdx), el.item.field, jel.val());
      });
    });
  });
});

There's a "not good" piece of code in the above snippet: taskStore.SetProperty. The hard-wiring to the taskStore is refactored out later so the binding is not specific to just the Task store!

Notice here we also use the record index to qualify the record. We do this because with this code jQuery("#template").html(task1 + task2 + task3); there are multiple elements with the same GUID because we've cloned the HTML template three times. Probably not ideal, but I'll live with that for now. In the meantime, the store I've created for the tasks:

let taskStore = store.CreateStore("Tasks", StoreType.LocalStorage);

manages setting the property value for the record at the specified index, and creating empty records as necessary:

public SetProperty(idx: number, property: string, value: any): StoreConfiguration {
  // Create additional records as necessary:
  while (this.data.length - 1 < idx) {
    this.data.push({});
  }

  this.data[idx][property] = value;
  this.UpdatePhysicalStorage(this.data[idx], property, value);

  return this;
}

private UpdatePhysicalStorage(record: any, property: string, value: string) : Store {
  switch (this.storeType) {
    case StoreType.InMemory:
      // Do nothing.
      break;

    case StoreType.RestCall:
      // Eventually send an update but we probably ought to have a PK 
     // with which to associate the change.
      break;

    case StoreType.LocalStorage:
      // Here we just update the whole structure.
      let json = JSON.stringify(this.data);
      window.localStorage.setItem(this.name, json);
      break;	
  }

  return this;
}

At the moment, this is implemented in the StoreConfiguration class. Seems awkward yet it's the StoreConfiguration class that maintains the data, whereas the Store class is really a "store manager", so probably Store should be called StoreManager and StoreConfiguration should be called Store! Gotta love refactoring to make the names of things clearer. So from hereon, that's what they'll be called. Rather a PITA to do without the "rename" feature when working with C# code!

After entering some values:

Image 10

we can see that these have been serialized to the local storage (inspecting local storage in Chrome):

Image 11

Cool, however notice that record 0 does not have a status, as I didn't change it from the default. What to do about that? This isn't an easy problem because we have a disconnect between the number of template instances we've created and the store data. So we need a mechanism to deal with that and set defaults. The simplest answer is to brute force that right now. At least it's explicit:

taskStore.SetProperty(0, "Status", taskStates[0].text);
taskStore.SetProperty(1, "Status", taskStates[0].text);
taskStore.SetProperty(2, "Status", taskStates[0].text);

So now, the task store is initialized with defaults:

Image 12

Ultimately, this only pushed the problem into the "ignored" bucket, as it's also dependent on the order of the status array. But no matter, let's push on and now that we have something in the store, let's load the UI with the store data! We also have the question of whether the store should be updated per keypress or only when the onchange event fires, which occurs when the element loses focus. Another "ignore for now" issue. Furthermore, we have an excellent demonstration of "don't implement code with side-effects!" in this function:

public SetProperty(idx: number, property: string, value: any): Store {
  // Create additional records as necessary:
  while (this.data.length - 1 < idx) {
    this.data.push({});
  }

  this.data[idx][property] = value;
  this.UpdatePhysicalStorage(this.data[idx], property, value);

  return this;
}

As updating the physical storage in the case of the local storage obliterates anything we've saved! I've created a bit of a conundrum -- if the records don't exist in the local storage, I want to set the defaults, but if they do exist, I don't want to set the defaults! So first, let's get rid of the side-effect and move the updating of the physical storage to the onchange handler:

jel.on('change', () => {
  let recIdx = Number(jel.attr("storeIdx"));
  let field = el.item.field;
  let val = jel.val();

  console.log("change for " + el.guid.ToString() + " at index " + 
              recIdx + " value of " + jel.val());
  taskStore.SetProperty(recIdx, field, val).UpdatePhysicalStorage(recIdx, field, val);
});

Next, this gets removed:

taskStore.SetProperty(0, "Status", taskStates[0].text);
taskStore.SetProperty(1, "Status", taskStates[0].text);
taskStore.SetProperty(2, "Status", taskStates[0].text);

and instead is replaced with the ability to set a default value if it doesn't exist, after the store has been loaded:

taskStore.Load()
  .SetDefault(0, "Status", taskStates[0].text)
  .SetDefault(1, "Status", taskStates[0].text)
  .SetDefault(2, "Status", taskStates[0].text)
  .Save();

which is implemented as:

public SetDefault(idx: number, property: string, value: any): Store {
  this.CreateNecessaryRecords(idx);

  if (!this.data[idx][property]) {
    this.data[idx][property] = value;
  }

  return this;
}

And the Save function:

public Save(): Store {
  switch (this.storeType) {
    case StoreType.InMemory:
      // TODO: throw exception?
      break;

    case StoreType.RestCall:
      // Eventually send an update but we probably ought to have a PK 
     // with which to associate the change.
      break;

    case StoreType.LocalStorage:
      // Here we just update the whole structure.
      this.SaveToLocalStorage();
      break;
  }

  return this;
}

However, this has the annoying effect of potentially making REST calls to save each record, even if nothing changed. Another "ignore this for now" issue, but we'll definitely need to implement a "field dirty" flag! For local storage, we have no choice, the entire structure must be saved, so for now we're good to go. When there's no local storage, we get the desired defaults:

Image 13

And when there is data, it's not obliterated by refreshing the page:

Image 14

Of course, the UI doesn't update because we need the binding to work the other way as well! A brute force implementation looks like this:

for (let i = 0; i < 3; i++) {
  for (let j = 0; j < builder.elements.length; j++) {
    let tel = builder.elements[j];
    let guid = tel.guid.ToString();
    let jel = jQuery(`[bindGuid = '${guid}'][storeIdx = '${i}']`);
    jel.val(taskStore.GetProperty(i, tel.item.field));
  }
}

Oooh, notice the template literal: let jel = jQuery(`[bindGuid = '${guid}'][storeIdx = '${i}']`); -- I'll have to refactor the code and use that more often!

This yields on page load:

Image 15

Cool, I can now create and save three tasks! Calling it quits for Day 4, back soon to work on reverse binding and better handling of defaults as well as getting rid of this silly "3 tasks" thing and making tasks more dynamic.

Day 5 - Store Callbacks

So that brute force approach above needs to be fixed, but I don't want the store to know anything about how the records fields map to UI elements, so I think what I'd like to do is provide callbacks for record and property level updates using the good ol' Inversion of Control principle. Possibly something like this should be done for the different store types as well so the application can override behavior per store. Later.

To the Store class, I'll add a couple callbacks with default "do nothing" handlers:

recordChangedCallback: (idx: number, record: any, store: Store) => void = () => { }; 
propertyChangedCallback: (idx: number, field: string, 
                         value: any, store: Store) => void = () => { };

and in the Load function, we'll call the recordChangedCallback for every record loaded (probably not what we want to do in the long run!):

this.data.forEach((record, idx) => this.recordChangedCallback(idx, record, this));

This gets wired in to the taskStore -- notice it's implemented so that it passes in the template builder, which is sort of like a view, so we can acquire all the field definitions in the "view" template:

taskStore.recordChangedCallback = 
  (idx, record, store) => this.UpdateRecordView(builder, store, idx, record);

and the handler looks a lot like the brute force approach above.

private UpdateRecordView(builder: TemplateBuilder, 
       store: Store, idx: number, record: any): void {
  for (let j = 0; j < builder.elements.length; j++) {
    let tel = builder.elements[j];
    let guid = tel.guid.ToString();
    let jel = jQuery(`[bindGuid = '${guid}'][storeIdx = '${idx}']`);
    let val = store.GetProperty(idx, tel.item.field);
    jel.val(val);
  }
}

This is a fairly generic approach. Let's do something similar for changing just a property and testing that by setting a record's property value via the store:

public SetProperty(idx: number, field: string, value: any): Store {
  this.CreateNecessaryRecords(idx);
  this.data[idx][field] = value;
  this.propertyChangedCallback(idx, field, value, this);  // <== this got added.

  return this;
}

Wired up like this:

taskStore.propertyChangedCallback = 
   (idx, field, value, store) => this.UpdatePropertyView(builder, store, idx, field, value);

And implemented like this:

private UpdatePropertyView(builder: TemplateBuilder, 
   store: Store, idx: number, field: string, value: any): void {
  let tel = builder.elements.find(e => e.item.field == field);
  let guid = tel.guid.ToString();
  let jel = jQuery(`[bindGuid = '${guid}'][storeIdx = '${idx}']`);
  jel.val(value);
}

Now we can set a property for a record in a store and it's reflected in the UI:

taskStore.SetProperty(1, "Task", `Random Task #${Math.floor(Math.random() * 100)}`);

Image 16

So let's look at adding and deleting tasks. Some of you are either laughing or groaning because I've backed myself into another corner with this "record index" concept, which makes deleting and inserting tasks a total nightmare because the storeIdx will go out of sync with the record it's managing. So it's time to throw out this whole concept in favor of a smarter way to handle records. At the moment, I've declared the store's data as an array of name:value pairs:

data: {}[] = [];

but it's time for something smarter -- a way to uniquely identify a record without using a row index, and a way to get that unique identifier associated with the UI elements. The irony here is that a numeric index is a fine way to do this, we just need to map the index to the physical record rather than assume a 1:1 correlation. We also no longer need the CreateNecessaryRecords method but instead we create only this single stub key:value object if the "index" is missing in the index-record map.

So instead, I now have:

private data: RowRecordMap = {};

It's private because I don't want anyone touching this structure, which is declared like this:

export interface RowRecordMap {
  [key: number]: {}
}

The most significant refactoring involved the record change callback:

jQuery.each(this.data, (k, v) => this.recordChangedCallback(k, v, this));

Pretty much nothing else changes because instead of the index being an array index, it's now a dictionary key and is therefore used in the same way. Here we assume that on an initial load, the record index (from 0 to n-1) corresponds 1:1 with the indices created by the template builder. One other important change is that to save to local storage, we don't want to save the key:value model, just the values, as the keys (the row index lookup) is completely arbitrary:

public GetRawData(): {}[] {
  return jQuery.map(this.data, value => value);
}

private SaveToLocalStorage() {
  let json = JSON.stringify(this.GetRawData());
  window.localStorage.setItem(this.storeName, json);
}

Deleting a Task

More refactoring! To make this work, each template that we're cloning needs to be wrapped in its own div so we can remove it. Currently, the HTML looks like this:

Image 17

Where the red box is one template instance. Instead, we want this (the code change to make this work was trivial so I'm not going to show it):

Image 18

Now let's reduce the width of the "Why" textbox and add a "Delete" button to the template definition:

{
  field: "Why",
  line: 1,
  width: "80%",			// <== Changed
  control: "textbox",
},
{
  text: "Delete",		// <== Added all this
  line: 1,
  width: "20%",
  control: "button",
}

And adding a Button method to the TemplateBuilder:

public Button(item: Item): TemplateBuilder {
  let guid = Guid.NewGuid();
  this.html += `<button type='button' style='width:100%' 

               storeIdx='{idx}' bindGuid='${guid.ToString()}>${item.text}</button>`;
  let el = new TemplateElement(item, guid);
  this.elements.push(el);

  return this;
}

We get this:

Image 19

Snazzy. Now we have to wire up the event! Uh, ok, how will this work? Well first, we need to wire up the click event:

switch (el.item.control) {
  case "button":
    jel.on('click', () => {
      let recIdx = Number(jel.attr("storeIdx"));
      console.log(`click for ${el.guid.ToString()} at index ${recIdx}`);
    });
    break;

  case "textbox":
  case "combobox":
    jel.on('change', () => {
      let recIdx = Number(jel.attr("storeIdx"));
      let field = el.item.field;
      let val = jel.val();

      console.log(`change for ${el.guid.ToString()} at index ${recIdx} 
                  with new value of ${jel.val()}`);
      storeManager.GetStore(el.item.associatedStoreName).SetProperty
                          (recIdx, field, val).UpdatePhysicalStorage(recIdx, field, val);
    });
    break;
}

And we can verify that it works by looking at the console log:

Image 20

Event Router

Given that this is all constructed by metadata, we need an event router which can route events to arbitrary but predefined functions in the code. This should be quite flexible but only if the code supports the behaviors we need.

So let's add a route property to the template:

{
  text: "Delete",
  line: 1,
  width: "20%",
  control: "button",
  route: "DeleteRecord",
}

Note that I don't call the route "deleteTask", because deleting a record should be handled in a very general purpose manner. The event router start is very simple:

import { Store } from "../classes/Store"
import { RouteHandlerMap } from "../interfaces/RouteHandlerMap"

export class EventRouter {
  routes: RouteHandlerMap = {};

  public AddRoute(routeName: string, fnc: (store: Store, idx: number) => void) {
    this.routes[routeName] = fnc;
  }

  public Route(routeName: string, store: Store, idx: number): void {
    this.routes[routeName](store, idx);
  }
}

The delete record handler is initialized:

let eventRouter = new EventRouter();
eventRouter.AddRoute("DeleteRecord", (store, idx) => store.DeleteRecord(idx));

A callback and the DeleteRecord function is added to the store:

recordDeletedCallback: (idx: number, store: Store) => void = () => { }; 
...
public DeleteRecord(idx: number) : void {
  delete this.data[idx];
  this.recordDeletedCallback(idx, this);
}

The delete record callback is initialized:

taskStore.recordDeletedCallback = (idx, store) => {
  this.DeleteRecordView(builder, store, idx);
  store.Save();
}

The router is invoked when the button is clicked:

case "button":
  jel.on('click', () => {
    let recIdx = Number(jel.attr("storeIdx"));
    console.log(`click for ${el.guid.ToString()} at index ${recIdx}`);
    eventRouter.Route(el.item.route, storeManager.GetStore(el.item.associatedStoreName), recIdx);
  });
break;

and the div wrapping the record is removed:

private DeleteRecordView(builder: TemplateBuilder, store: Store, idx: number): void {
  jQuery(`[templateIdx = '${idx}']`).remove();
}

Ignoring:

  • The "templateIdx" attribute name for now, which obviously has to be specified somehow to support more than one template entity type.
  • That this removes the entire div as opposed to, say, clearing the fields or removing a row from a grid, this works nicely.
  • That the Save call doesn't have a clue as to how to send a REST call to delete the specific record.

We can mosey on along and after clicking on the delete button for second task, T2, we now see:

Image 21

and our local storage looks like this:

Image 22

Now let's refactor the load process so that the callback dynamically creates the template instances, which will be a precursor to inserting a new task. First, the recordCreatedCallback is renamed to recordCreatedCallback, which is a much better name! Then, we're going to remove this prototyping code:

let task1 = this.SetStoreIndex(html, 0);
let task2 = this.SetStoreIndex(html, 1);
let task3 = this.SetStoreIndex(html, 2);
jQuery("#template").html(task1 + task2 + task3);

because our template "view" is going to be created dynamically as records are loaded. So now the CreateRecordView function looks like this:

private CreateRecordView(builder: TemplateBuilder, store: Store, 
                        idx: number, record: {}): void {
  let html = builder.html;
  let template = this.SetStoreIndex(html, idx);
  jQuery("#template").append(template);

  for (let j = 0; j < builder.elements.length; j++) {
    let tel = builder.elements[j];
    let guid = tel.guid.ToString();
    let jel = jQuery(`[bindGuid = '${guid}'][storeIdx = '${idx}']`);
    let val = record[tel.item.field];
    jel.val(val);
  }
}

Inserting Tasks

And because in testing, I obliterated all my tasks, I now have to implement a Create Task button! The events for all elements in the template will also need to be wired up every time we create a task! First, the HTML:

<button type="button" id="createTask">Create Task</button>
<div id="template" style="width:40%"></div>

Then wiring up the event partly using the event router:

jQuery("#createTask").on('click', () => {
  let idx = eventRouter.Route("CreateRecord", taskStore, 0); // insert at position 0
  taskStore.SetDefault(idx, "Status", taskStates[0].text);
  taskStore.Save();
});

and the route definition:

eventRouter.AddRoute("CreateRecord", (store, idx) => store.CreateRecord(true));

and the implementation in the store:

public CreateRecord(insert = false): number {
  let nextIdx = 0;

  if (this.Records() > 0) {
    nextIdx = Math.max.apply(Math, Object.keys(this.data)) + 1;
  }

  this.data[nextIdx] = {};
  this.recordCreatedCallback(nextIdx, {}, insert, this);

  return nextIdx;
}

Notice how we obtain a "unique" record "index", and how we can specify whether to insert at the beginning or append to the end, not of the data records (these are order independent) but the flag gets passed on to the "view" that handles where the template should be created, so once again we refactor CreateRecordView:

private CreateRecordView(builder: TemplateBuilder, store: Store, 
                        idx: number, record: {}, insert: boolean): void {
  let html = builder.html;
  let template = this.SetStoreIndex(html, idx);

  if (insert) {
    jQuery("#template").prepend(template);
  } else {
    jQuery("#template").append(template);
  }

  this.BindSpecificRecord(builder, idx);

  for (let j = 0; j < builder.elements.length; j++) {
    let tel = builder.elements[j];
    let guid = tel.guid.ToString();
    let jel = jQuery(`[bindGuid = '${guid}'][storeIdx = '${idx}']`);
    let val = record[tel.item.field];
    jel.val(val);
  }
}

I'm not going to show you the BindSpecificRecord function because it's almost identical to the binding that occurs in the document ready event, and so all that common code needs to be refactored before I show it to you! One odd behavior that I'm saving for the next day is that when the template is created this way, the combobox doesn't default to "TODO" - will have to figure out why. Regardless, starting from a blank slate:

Image 23

I created two tasks, note how they are in reverse order because tasks are prepended in the UI:

Image 24

and we can see that they are appended in the local storage:

Image 25

This, of course, causes a problem when the page is refreshed:

Image 26

The order got changed! Hmmm...

Now, from demos I've seen of Vue and other frameworks, doing what has taken 5 days to accomplish here is probably a 30 minute exercise in Vue. However, the point here is that I'm actually building the framework and the application together, and quite frankly, having a lot of fun doing it! So that's all that counts! End of Day 5, and I can finally create, edit, and delete tasks!

Day 6 - Basic Relationships

So this is one of those "rubber meets the road" moments. I'm going to add a couple relationships. Software is not monogamous! I'd like to add contacts and notes that are child entities of the task. My "tasks" are usually integration level tasks (they probably should be called projects instead of tasks!), like "add this credit card processor", which means that I have a bunch of people that I'm talking to, and I want to be able to find them as related to the task. Same with notes, I want to make notes of conversations, discoveries and so forth related to the task. Why this will be a "rubber meets the road" moment is because I currently have no mechanism for identifying and relating together two entities, such as a task and a note. It'll also mean dealing with some hardcoded tags, like here:

if (insert) {
  jQuery("#template").prepend(template);
} else {
  jQuery("#template").append(template);
}

The function needs to be general purpose and therefore the div associated with the entity has to be figured out, not hard-coded. So this makes more sense:

if (insert) {
  jQuery(builder.templateContainerID).prepend(template);
} else {
  jQuery(builder.templateContainerID).append(template);
}

Also, the store event callbacks are general purpose, so we can do this:

this.AssignStoreCallbacks(taskStore, taskBuilder);
this.AssignStoreCallbacks(noteStore, noteBuilder);
...
private AssignStoreCallbacks(store: Store, builder: TemplateBuilder): void {
  store.recordCreatedCallback = (idx, record, insert, store) => 
                this.CreateRecordView(builder, store, idx, record, insert);
  store.propertyChangedCallback = (idx, field, value, store) => 
                this.UpdatePropertyView(builder, store, idx, field, value);
  store.recordDeletedCallback = (idx, store) => {
    this.DeleteRecordView(builder, store, idx);
    store.Save();
  }
}

This also needs to be fixed:

private DeleteRecordView(builder: TemplateBuilder, store: Store, idx: number): void {
  jQuery(`[templateIdx = '${idx}']`).remove();
}

because the index number is not sufficient to determine the associated entity unless it's also qualified by the container name:

private DeleteRecordView(builder: TemplateBuilder, store: Store, idx: number): void {
  let path = `${builder.templateContainerID} > [templateIdx='${idx}']`;
  jQuery(path).remove();
}

But of course, this assumes that the UI will have unique container names. This leads us to the HTML that defines the layout -- templates must be in containers:

<div class="entitySeparator">
  <button type="button" id="createTask" class="createButton">Create Task</button>
  <div id="taskTemplateContainer" class="templateContainer"></div>
</div>
<div class="entitySeparator">
  <button type="button" id="createNote" class="createButton">Create Note</button>
  <div id="noteTemplateContainer" class="templateContainer"></div>
</div>

At this point, I can create tasks and notes:

Image 27

and they persist quite nicely in the local storage as well:

Image 28

To figure out next:

  1. Some unique ID field in the record that is persisted. Normally this would be the primary key, but we're not saving the data to a database and I'd like the unique ID to be decoupled from the database's PK, particularly if the user is working disconnected from the Internet, which we should be able to fairly easily support.
  2. Clicking on the parent (the task in our case) should bring up the specific child records.
  3. Do we have separate stores (like "Task-Note" and "Task-Contact") for each parent-child relationship or do we create a "metastore" with parent-child entity names and this unique ID? Or do we create a hierarchical structure where, say, a task has child elements such as notes?
  4. How do we indicate to the user the selected parent that will be associated with the child entities?

Regard #4, I like an unobtrusive approach like this, where the green left border indicates the record that's been selected.

Image 29

The trick here is that we want to remove the selection only for the entity records associated with the selection:

private RecordSelected(builder: TemplateBuilder, recIdx: number): void {
  jQuery(builder.templateContainerID).children().removeClass("recordSelected");
  let path = `${builder.templateContainerID} > [templateIdx='${recIdx}']`;
  jQuery(path).addClass("recordSelected");
}

This way, we can select a record for each entity type:

Image 30

Regarding #3, a hierarchical structure is out of the question, as it potentially creates a highly denormalized dataset. Consider that a task (or if I want to add projects at some point, a project) may have the same contact information. If I update the contact, do I want find all the occurrences in an arbitrary hierarchy where that contact exists and update each and every one of them? What if I delete a contact because that person no longer works at that company? Heck no. And separate parent-child stores is rejected because of the number of local storage items (or database tables) that it requires. Particularly when it comes to database tables, the last thing I want to do is create parent-child tables on the fly. So a single meta-store that manages the mappings of all parent-child relationships seems most reasonable at the moment, the major consideration is the performance when the "table" contains potentially thousands (or magnitudes more) of relationships. At this point, such a scenario doesn't need to be considered.

Here, we have our first concrete model:

export class ParentChildRelationshipModel {
  parent: string;
  child: string;
  parentId: number;
  childId: number;
}

Notice that the parent and child IDs are numbers. The maximum number is 21024, the problem though is that the Number type is a 64-bit floating point value, so it's not the range but the precision that is of concern. I'm guessing that finding parent-child relationships by a number ID rather than, say, a GUID ID, will be faster and that I don't have to worry about precision too much at this point.

And (horrors), similar to ExtJS, we actually have a concrete ParentChildStore which will have a function for acquiring a unique number ID:

import { Store } from "../classes/Store"

export class ParentChildStore extends Store {
}

The parent-child store is created a little bit differently:

let parentChildRelationshipStore = 
   new ParentChildStore(storeManager, StoreType.LocalStorage, "ParentChildRelationships");
storeManager.RegisterStore(parentChildRelationshipStore);

And we can access a concrete store type using this function, note the comments:

public GetTypedStore<T>(storeName: string): T {
  // Compiler says: Conversion of type 'Store' to type 'T' may be a mistake because 
  // neither type sufficiently overlaps with the other. If this was intentional, 
  // convert the expression to 'unknown' first.
  // So how do I tell it that T must extended from Store?
  return (<unknown>this.stores[storeName]) as T;
}

In C#, I would write something like GetStore<T>(string storeName) where T : Store and the downcast to T would work fine, but I have no idea how to do this in TypeScript.

While I need a persistable counter, like a sequence, to get the next ID, let's look at the CreateRecord function first:

public CreateRecord(insert = false): number {
  let nextIdx = 0;

  if (this.Records() > 0) {
    nextIdx = Math.max.apply(Math, Object.keys(this.data)) + 1;
  }

  this.data[nextIdx] = {};        <== THIS LINE IN PARTICULAR
  this.recordCreatedCallback(nextIdx, {}, insert, this);

  return nextIdx;
}

It's the assignment of the empty object that needs to set an ID, but I don't want to code that in the store -- I prefer to have that decoupled, so I'll implement it as a call to the StoreManager which will then invoke a callback to the application, so the unique record identifier can be something that the application manages. We could even do a "per store" callback, but that's unnecessary at this point. So now the store calls:

this.data[nextIdx] = this.storeManager.GetPrimaryKey();

The definition for the callback is crazy looking, in that it defaults to returning {}:

getPrimaryKeyCallback: () => any = () => {};

and for testing, let's just implement a basic counter:

storeManager = new StoreManager();

// For testing:
let n = 0;
storeManager.getPrimaryKeyCallback = () => {
  return { __ID: ++n };
}

and we can see that this creates the primary key key-value pair when I create a task!

Image 31

So this is the end of Day 6. I still need to persist the sequence, probably a "Sequence" store that allows me to define different sequences, and of course, create the parent-child records and the UI behavior. Getting there!

Day 7 - Sequence Store and the Parent-Child Relationship Store

So a sequence store seems like a good idea. Again, this can be a concrete model and store. The model:

export class SequenceModel {
  key: string;
  n: number;

  constructor(key: string) {
    this.key = key;
    this.n = 0;
  }
}

The Sequence store:

import { Store } from "../classes/Store"
import { SequenceModel } from "../models/SequenceModel"

export class SequenceStore extends Store {
  GetNext(skey: string): number {
    let n = 0;
    let recIdx = this.FindRecordOfType<SequenceModel>(r => r.key == skey);
    
    if (recIdx == -1) {
      recIdx = this.CreateRecord();
      this.SetProperty(recIdx, "key", skey);
      this.SetProperty(recIdx, "count", 0);
    }

    n = this.GetProperty(recIdx, "count") + 1;
    this.SetProperty(recIdx, "count", n);
    this.Save();

    return n;
  }
}

and the FindRecordOfType function:

public FindRecordOfType<T>(where: (T) => boolean): number {
  let idx = -1;

  for (let k of Object.keys(this.data)) {
    if (where(<T>this.data[k])) {
      idx = parseInt(k);
      break;
    }
  }

  return idx;
}

We can write a simple test:

let seqStore = new SequenceStore(storeManager, StoreType.LocalStorage, "Sequences");
storeManager.RegisterStore(seqStore);
seqStore.Load();
let n1 = seqStore.GetNext("c1");
let n2 = seqStore.GetNext("c2");
let n3 = seqStore.GetNext("c2");

and in the local storage, we see:

Image 32

so we can now assign sequences to each of the stores:

storeManager.getPrimaryKeyCallback = (storeName: string) => {
  return { __ID: seqStore.GetNext(storeName) };

Except that creating the sequence results in infinite recursion, because the sequence record is trying to get its own primary key!!!

Image 33

Oops!

The simplest way to deal with this is make the method overridable in the base class, first by refactoring the CreateRecord function:

public CreateRecord(insert = false): number {
  let nextIdx = 0;

  if (this.Records() > 0) {
    nextIdx = Math.max.apply(Math, Object.keys(this.data)) + 1;
  }

  this.data[nextIdx] = this.GetPrimaryKey();
  this.recordCreatedCallback(nextIdx, {}, insert, this);

  return nextIdx;
}

Defining the default behavior:

protected GetPrimaryKey(): {} {
  return this.storeManager.GetPrimaryKey(this.storeName);
}

and overriding it in the SequenceStore:

protected GetPrimaryKey(): {} {
  return {};
}

Problem solved!

Making the Association

To make the association between parent and child record, we'll add a field to hold the selected record index in the store:

selectedRecordIndex: number = undefined; // multiple selection not allowed.

And in the BindElementEvents function, where we call RecordSelected, we'll add setting this field in the store:

jel.on('focus', () => {
  this.RecordSelected(builder, recIdx));
  store.selectedRecordIndex = recIdx;
}

In the event handler for the button responsible for create a task note:

jQuery("#createTaskNote").on('click', () => {
  let idx = eventRouter.Route("CreateRecord", noteStore, 0); // insert at position 0
  noteStore.Save();
});

We'll add a call to add the parent-child record:

jQuery("#createTaskNote").on('click', () => {
  let idx = eventRouter.Route("CreateRecord", noteStore, 0); // insert at position 0
  parentChildRelationshipStore.AddRelationship(taskStore, noteStore, idx); // <=== Added this
  noteStore.Save();
});

With the implementation:

AddRelationship(parentStore: Store, childStore: Store, childRecIdx: number): void {
  let parentRecIdx = parentStore.selectedRecordIndex;

  if (parentRecIdx !== undefined) {
    let recIdx = this.CreateRecord();
    let parentID = parentStore.GetProperty(parentRecIdx, "__ID");
    let childID = childStore.GetProperty(childRecIdx, "__ID");
    let rel = new ParentChildRelationshipModel
             (parentStore.storeName, childStore.storeName, parentID, childID);
    this.SetRecord(recIdx, rel);
    this.Save();
  } else {
    // callback that parent record needs to be selected?
    // or throw an exception?
  }
}

And there we have it:

Image 34

Now we just have to select the correct children for the selected parent. Having already defined a global variable (ugh) for declaring relationships:

var relationships : Relationship = [
  {
    parent: "Tasks",
    children: ["Notes"]
  }
];

Where Relationship is defined as:

export interface Relationship {
  parent: string;
  children: string[];
}

We can now tie in to the same "selected" event handler to acquire the specific child relationships, remove any previous ones, and show just the specific ones for the selected record. We also don't want to go through this process every time a field in the record is selected.

jel.on('focus', () => {
  if (store.selectedRecordIndex != recIdx) {
    this.RecordSelected(builder, recIdx);
    store.selectedRecordIndex = recIdx;
    this.ShowChildRecords(store, recIdx, relationships);
  }
});

In the ParentChildStore, we can define:

GetChildInfo(parent: string, parentId: number, child: string): ChildRecordInfo {
  let childRecs = this.FindRecordsOfType<ParentChildRelationshipModel>
   (rel => rel.parent == parent && rel.parentId == parentId && rel.child == child);
  let childRecIds = childRecs.map(r => r.childId);
  let childStore = this.storeManager.GetStore(child);

  // Annoying. VS2017 doesn't have an option for ECMAScript 7
  let recs = childStore.FindRecords(r => childRecIds.indexOf((<any>r).__ID) != -1);

  return { store: childStore, childrenIndices: recs };
}

In the Store class, we implement:

public FindRecords(where: ({ }) => boolean): number[] {
  let recs = [];

  for (let k of Object.keys(this.data)) {
    if (where(this.data[k])) {
      recs.push(k);
    }
  }

  return recs;
}

This returns the record indices, which we need to populate the template {idx} value so we know what record is being edited.

This lovely function has the job of finding the children and populating the templates (some refactoring occurred here, for example, mapping a store to its builder):

private ShowChildRecords
 (parentStore: Store, parentRecIdx: number, relationships: Relationship[]): void {
  let parentStoreName = parentStore.storeName;
  let parentId = parentStore.GetProperty(parentRecIdx, "__ID");
  let relArray = relationships.filter(r => r.parent == parentStoreName);

  // Only one record for the parent type should exist.
  if (relArray.length == 1) {
    let rel = relArray[0];

    rel.children.forEach(child => {
      let builder = builders[child].builder;
      this.DeleteAllRecordsView(builder);
      let childRecs = 
         parentChildRelationshipStore.GetChildInfo(parentStoreName, parentId, child);
      let childStore = childRecs.store;

      childRecs.childrenIndices.map(idx => Number(idx)).forEach(recIdx => {
        let rec = childStore.GetRecord(recIdx);
        this.CreateRecordView(builder, childStore, recIdx, rec, false);
      });
    });
  }
}

And it works! Clicking on Task 1, where I created 2 notes:

Image 35

Clicking on Task 2, where I created 1 note:

Image 36

Contacts

Now let's have fun and create another child, Contacts.

Update the relationship map:

var relationships : Relationship[] = [
  {
    parent: "Tasks",
    children: ["Contacts", "Notes"]
  }
];

Update the HTML:

<div class="entitySeparator">
  <button type="button" id="createTask" class="createButton">Create Task</button>
  <div id="taskTemplateContainer" class="templateContainer"></div>
</div>
<div class="entitySeparator">
  <button type="button" id="createTaskContact" class="createButton">Create Contact</button>
  <div id="contactTemplateContainer" class="templateContainer"></div>
</div>
<div class="entitySeparator">
  <button type="button" id="createTaskNote" class="createButton">Create Note</button>
  <div id="noteTemplateContainer" class="templateContainer"></div>
</div>

Create the contact template:

let contactTemplate = [
  { field: "Name", line: 0, width: "50%", control: "textbox" },
  { field: "Email", line: 0, width: "50%", control: "textbox" },
  { field: "Comment", line: 1, width: "100%", control: "textbox" },
  { text: "Delete", line: 1, width: "20%", control: "button", route: "DeleteRecord" }
];

Create the store:

let contactStore = storeManager.CreateStore("Contacts", StoreType.LocalStorage);

Create the builder:

let contactBuilder = this.CreateHtmlTemplate
 ("#contactTemplateContainer", contactTemplate, storeManager, contactStore.storeName);

Assign the callbacks:

this.AssignStoreCallbacks(contactStore, contactBuilder);

Add the relationship:

jQuery("#createTaskContact").on('click', () => {
  let idx = eventRouter.Route("CreateRecord", contactStore, 0); // insert at position 0
  parentChildRelationshipStore.AddRelationship(taskStore, contactStore, idx);
  contactStore.Save();
});

Load the contacts but don't render them on the view (prevent the callback in other words):

taskStore.Load();
noteStore.Load(false);
contactStore.Load(false);

And there we are: we've just added another child entity to Tasks!

Image 37

Now, having gone through that exercise, with the exception of the HTML to hold the contacts and the contact template itself, all the rest of the stuff we manually did can be handled with a function call, which will be Day 8. We also have to deal with deleting the relationship entry when a child is deleted, and deleting all the child relationships when a parent is deleted. Goodnight!

Day 8 - Simplifying the Create View Steps

First, let's create a function that takes all those discrete setup steps and rolls them into one call with a lot of parameters:

private CreateStoreViewFromTemplate(
  storeManager: StoreManager,
  storeName: string,
  storeType: StoreType,
  containerName: string,
  template: Items,
  createButtonId: string,
  updateView: boolean = true,
  parentStore: Store = undefined,
  createCallback: (idx: number, store: Store) => void = _ => { }
): Store {
  let store = storeManager.CreateStore(storeName, storeType);
  let builder = this.CreateHtmlTemplate(containerName, template, storeManager, storeName);
  this.AssignStoreCallbacks(store, builder);

  jQuery(document).ready(() => {
    if (updateView) {
      this.BindElementEvents(builder, _ => true);
    }

    jQuery(createButtonId).on('click', () => {
      let idx = eventRouter.Route("CreateRecord", store, 0); // insert at position 0
      createCallback(idx, store);

      if (parentStore) {
        parentChildRelationshipStore.AddRelationship(parentStore, store, idx);
      }

      store.Save();
    });
  });

  store.Load(updateView);

  return store;
}

This "simplifies" the creation process to four steps:

  1. Define the template.
  2. Define the container.
  3. Update the relationship map.
  4. Create the store view.

Step 4 is now written as:

let taskStore = this.CreateStoreViewFromTemplate(
  storeManager, 
  "Tasks", 
  StoreType.LocalStorage, 
  "#taskTemplateContainer", 
  taskTemplate, 
  "#createTask", 
  true, 
  undefined, 
  (idx, store) => store.SetDefault(idx, "Status", taskStates[0].text));

this.CreateStoreViewFromTemplate(
  storeManager, 
  "Notes", 
  StoreType.LocalStorage, 
  "#noteTemplateContainer", 
  noteTemplate, 
  "#createTaskNote", 
  false, 
  taskStore);

this.CreateStoreViewFromTemplate(
  storeManager, 
  "Contacts", 
  StoreType.LocalStorage, 
  "#contactTemplateContainer", 
  contactTemplate, 
  "#createTaskContact", 
  false, 
  taskStore);

OK, a lot of parameters, but it's a highly repeatable pattern.

Next, we want to delete any relationships. The relationship needs to be deleted before the record is deleted because we need access to the __ID field, so we have to reverse the way the callback is handled in the Store to:

public DeleteRecord(idx: number) : void {
  this.recordDeletedCallback(idx, this);
  delete this.data[idx];
}

which will also allow for recursively deleting the entire hierarchy of an element when the element is deleted.

Then, in the callback handler:

store.recordDeletedCallback = (idx, store) => {
  parentChildRelationshipStore.DeleteRelationship(store, idx);
  this.DeleteRecordView(builder, idx);
}

But we also have to save the store now in the route handler because the callback, which was performing the save, is being called before the record is deleted:

eventRouter.AddRoute("DeleteRecord", (store, idx) => {
  store.DeleteRecord(idx);
  store.Save();
});

and the implementation in the ParentChildStore:

public DeleteRelationship(store: Store, recIdx: number) {
  let storeName = store.storeName;
  let id = store.GetProperty(recIdx, "__ID");
  let touchedStores : string[] = []; // So we save the store only once after this process.

  // safety check.
  if (id) {
    let parents = this.FindRecordsOfType<ParentChildRelationshipModel>
                 (rel => rel.parent == storeName && rel.parentId == id);
    let children = this.FindRecordsOfType<ParentChildRelationshipModel>
                  (rel => rel.child == storeName && rel.childId == id);

    // All children of the parent are deleted.
    parents.forEach(p => {
      this.DeleteChildrenOfParent(p, touchedStores);
    });

    // All child relationships are deleted.
    children.forEach(c => {
      let relRecIdx = 
     this.FindRecordOfType<ParentChildRelationshipModel>((r: ParentChildRelationshipModel) =>
      r.parent == c.parent &&
      r.parentId == c.parentId &&
      r.child == c.child &&
      r.childId == c.childId);
    this.DeleteRecord(relRecIdx);
    });
  } else {
    console.log(`Expected to have an __ID value in store ${storeName} record index: ${recIdx}`);
  }

  // Save all touched stores.
  touchedStores.forEach(s => this.storeManager.GetStore(s).Save());

  this.Save();
}

with a helper function:

private DeleteChildrenOfParent
 (p: ParentChildRelationshipModel, touchedStores: string[]): void {
  let childStoreName = p.child;
  let childId = p.childId;
  let childStore = this.storeManager.GetStore(childStoreName);
  let recIdx = childStore.FindRecord(r => (<any>r).__ID == childId);

  // safety check.
  if (recIdx != -1) {
    // Recursive deletion of child's children will occur (I think - untested!)
    childStore.DeleteRecord(recIdx);

    if (touchedStores.indexOf(childStoreName) == -1) {
      touchedStores.push(childStoreName);
    }
  } else {
    console.log(`Expected to find record in store ${childStoreName} with __ID = ${childId}`);
  }

  // Delete the parent-child relationship.
  let relRecIdx = 
   this.FindRecordOfType<ParentChildRelationshipModel>((r: ParentChildRelationshipModel) =>
    r.parent == p.parent &&
    r.parentId == p.parentId &&
    r.child == p.child &&
    r.childId == childId);

  this.DeleteRecord(relRecIdx);
}

Day 9: Bugs

So in creating a more rich relationship model:

var relationships : Relationship[] = [
{
  parent: "Projects",
  children: ["Tasks", "Contacts", "Notes"]
},
{
  parent: "Tasks",
  children: ["Notes"]
}
];

in which Notes are children of both Projects and Tasks, a couple bugs came up.

Bug: Create a Store Only Once

First is the issue that I was creating the Notes store twice, which is fixed checking if the store exists:

private CreateStoreViewFromTemplate(
...
): Store {

// ?. operator. 
// Supposedly TypeScript 3.7 has it, but I can't select that version in VS2017. VS2019?
let parentStoreName = parentStore && parentStore.storeName || undefined;
let builder = this.CreateHtmlTemplate
             (containerName, template, storeManager, storeName, parentStoreName);
let store = undefined;

if (storeManager.HasStore(storeName)) {
  store = storeManager.GetStore(storeName);
} else {
  store = storeManager.CreateStore(storeName, storeType);
  this.AssignStoreCallbacks(store, builder);
}

Bug: Associate the Builder with the Correct Parent-Child Context

Second, the builder has to be parent-child aware so that "Create Task Note" uses the Task-Note builder, not the Project-Note builder. This was easy enough (though sort of kludgy) to fix:

private GetBuilderName(parentStoreName: string, childStoreName: string): string {
  return (parentStoreName || "") + "-" + childStoreName;
}

And...

private CreateHtmlTemplate(templateContainerID: string, template: Items, 
 storeManager: StoreManager, storeName: string, parentStoreName: string): TemplateBuilder {
  let builder = new TemplateBuilder(templateContainerID);
  let builderName = this.GetBuilderName(parentStoreName, storeName);
  builders[builderName] = { builder, template: templateContainerID };
  ...

Bug: Associate the CRUD Operations with the Correct Builder Context

The third problem is more insidious, in the call to AssignStoreCallbacks:

private AssignStoreCallbacks(store: Store, builder: TemplateBuilder): void {
  store.recordCreatedCallback = 
   (idx, record, insert, store) => this.CreateRecordView(builder, store, idx, insert);
  store.propertyChangedCallback = 
   (idx, field, value, store) => this.UpdatePropertyView(builder, store, idx, field, value);
  store.recordDeletedCallback = (idx, store) => {
    parentChildRelationshipStore.DeleteRelationship(store, idx);
    this.DeleteRecordView(builder, idx);
  }
}

The problem here is that the builder is the one associated with the store when the store is first created. The bug is that because this is the Notes store for the Project-Notes builder, adding a Task-Note adds the note to the Project-Notes instead! Two things need to happen:

  1. There should only be one callback for the store.
  2. But the builder must be specific to the "context" of the CRUD operation.

The fix for this is to pass into the store the "context" for the CRUD operations. At the moment, I'm just passing in the TemplateBuilder instance because I'm too lazy to create a Context class and I'm not sure it's needed:

The upshot of it is that the CRUD callbacks now get the builder context which they pass along to the handler:

private AssignStoreCallbacks(store: Store): void {
  store.recordCreatedCallback = 
 (idx, record, insert, store, builder) => this.CreateRecordView(builder, store, idx, insert);
  store.propertyChangedCallback = (idx, field, value, store, builder) => 
              this.UpdatePropertyView(builder, store, idx, field, value);
  store.recordDeletedCallback = (idx, store, builder) => {
    parentChildRelationshipStore.DeleteRelationship(store, idx);
    this.DeleteRecordView(builder, idx);
  }
}

Two Bugs, Same Solution

  • Grandchild Views need to be removed when Child List changes
  • Deleting a Parent should remove Child Template Views

If I create two projects with different tasks and task notes, where the task note is the grandchild, when I select a different project, the project children update (the project tasks) but the task notes remain on-screen, which leads to a lot of confusion. The function ShowChildRecords is great, but we need to remove grandchild records as the child context has changed. So this piece of code:

jel.on('focus', () => {
  if (store.selectedRecordIndex != recIdx) {
    this.RecordSelected(builder, recIdx);
    store.selectedRecordIndex = recIdx;
    this.ShowChildRecords(store, recIdx, relationships);
  }
});

gets an additional function call:

jel.on('focus', () => {
  if (store.selectedRecordIndex != recIdx) {
    this.RemoveChildRecordsView(store, store.selectedRecordIndex);
    this.RecordSelected(builder, recIdx);
    store.selectedRecordIndex = recIdx;
    this.ShowChildRecords(store, recIdx, relationships);
  }
});

which is implemented as:

// Recursively remove all child view records.
private RemoveChildRecordsView(store: Store, recIdx: number): void {
  let storeName = store.storeName;
  let id = store.GetProperty(recIdx, "__ID");
  let rels = relationships.filter(r => r.parent == storeName);

  if (rels.length == 1) {
    let childEntities = rels[0].children;

    childEntities.forEach(childEntity => {
      if (storeManager.HasStore(childEntity)) {
        var info = parentChildRelationshipStore.GetChildInfo(storeName, id, childEntity);
        info.childrenIndices.forEach(childRecIdx => {
          let builderName = this.GetBuilderName(storeName, childEntity);
          let builder = builders[builderName].builder;
          this.DeleteRecordView(builder, childRecIdx);
          this.RemoveChildRecordsView(storeManager.GetStore(childEntity), childRecIdx);
        });
      }
    });
  }
}

Bug: The Selected Record is Parent-Child Dependent

Note: The following thought process is WRONG! I'm keeping this in here because it was something I thought was wrong and only on further reflection did I realize it was not wrong. Unit tests would validate my belief that the writeup here is incorrect!

So here goes in the wrong thinking:

When a store is shared between two different parents, the selected record is specific to the parent-child relationship, not the store!

Question: Is Parent-Child Sufficient to Describe the Uniqueness and Entity?

No. For example, if I have a parent-child relationship B-C, and a hierarchy of A-B-C and D-B-C, the specific context of the records in C is associated with its relationship to B's records. And while B's context is in relationship to A's records, the selected record for the store depends on whether the entity path is A-B-C or D-B-C. Please realize that "A" and "D" different entity types, not different records of the same entity.

Even the template builder name is not a 2-level parent-child relationship. This works so far because the relationships are all uniquely defined with two levels of hierarchy. But insert another top level to the hierarchy and the template builder name's relationship to the builder (and the specific templateContainerID with which the builder is associated) fails.

Solution

This means that if we don't want to keep fixing up the code, we have to have a general purpose solution to the issue of identifying:

  1. The correct builder
  2. The selected record

as they are associated with the entity type hierarchy, no matter how deep. Keep in mind that the parent-child relationship model is still valid because it is associating relationships between parent and child entity instances whereas the builder and UI management is working often with the entity type hierarchy.

Why This is Not a Bug

First, when we load the records of parent-child relationship, it is qualified by the parent ID, which is unique:

let childRecs = parentChildRelationshipStore.GetChildInfo(parentStoreName, parentId, child);

and in the GetChildInfo function:

let childRecs = this.FindRecordsOfType<ParentChildRelationshipModel>
 (rel => rel.parent == parent && rel.parentId == parentId && rel.child == child);

But What is a Bug is This

In the above two items, "the correct builder" and "the selected record", the correct builder must be determined by the entity type hierarchy which needs the full path to determine the template container, but the selected record is associated with the instance and so is not actually the issue.

The code identifies the appropriate builder, which includes the HTML container template name, using:

let builderName = this.GetBuilderName(parentStoreName, child);

which is determined by:

private GetBuilderName(parentStoreName: string, childStoreName: string): string {
  return (parentStoreName || "") + "-" + childStoreName;
}

So here, we see that the builder associated with B-C does not have enough information to determine the template container for A-B-C vs. D-B-C. And that's where the real bug is. The upshot of this is that it's very important to distinguish between type and instance.

This will be addressed in Day 12, The Parent-Child Template Problem.

Nicety: Focus on First Field when Adding a Record

Trying to avoid unnecessary clicks, this:

private FocusOnFirstField(builder: TemplateBuilder, idx: number) {
  let tel = builder.elements[0];
  let guid = tel.guid.ToString();
  jQuery(`[bindGuid = '${guid}'][storeIdx = '${idx}']`).focus();
}

when called here:

store.recordCreatedCallback = (idx, record, insert, store, builder) => {
  this.CreateRecordView(builder, store, idx, insert);
  this.FocusOnFirstField(builder, idx);
};

makes life a lot nicer.

Day 10: A Few More Niceties

So I've also added links at the project and task level so I can reference internal and online links that are related to the project:

var relationships : Relationship[] = [
  {
    parent: "Projects",
    children: ["Tasks", "Contacts", "Links", "Notes"]
  },
  {
    parent: "Tasks",
    children: ["Links", "Notes"]
  }
];

And the related HTML and template were created as well.

This is How Life Should Work

Just now, I also decided I wanted to add "Title" to the Contact. So all I did was add this line to the contactTemplate:

{ field: "Title", line: 0, width: "30%", control: "textbox" },

Done. What didn't have to happen was that I didn't have to change some model definition of the client-side. And of course, I didn't have to implement a DB-schema migration, and I didn't have to change some EntityFramework or Linq2SQL entity model in C#. Frankly, when I add server-side database support, I still don't want to do any of that stuff! I should be able to touch one place and one place only: the template that describes what fields I want to see and where they are. Everything else should just figure out how to adjust.

Day 11: Colorizing Status

This is a bit of a hack, but I want to visually indicate the status of a project and task by colorizing the dropdown:

Image 38

This didn't take all day, it's just the time I had available.

Implemented by handling the change, focus, and blur events -- when the dropdown gets focus, it goes back to white so the entire selection list doesn't have the background color of the current status:

case "combobox":
  jel.on('change', () => {
    // TODO: Move this very custom behavior out into a view handler
    let val = this.SetPropertyValue(builder, jel, el, recIdx);
    this.SetComboboxColor(jel, val);
  });

  // I can't find an event for when the option list is actually shown, so for now 
  // we reset the background color on focus and restore it on lose focus.
  jel.on('focus', () => {
    jel.css("background-color", "white");
  });

  jel.on('blur', () => {
    let val = jel.val();
    this.SetComboboxColor(jel, val);
  });
  break;

and when the record view is created:

private CreateRecordView(builder: TemplateBuilder, store: Store, 
                        idx: number, insert: boolean): void {
  ...
 // Hack!
  if (tel.item.control == "combobox") {
    this.SetComboboxColor(jel, val);
  }
}

Day 12 - The Parent-Child Template Problem

So this:

private GetBuilderName(parentStoreName: string, childStoreName: string): string {
  return (parentStoreName || "") + "-" + childStoreName;
}

is a hack. The global variables are also a hack, as is storing the selected record index in the store -- it should be associated with the view controller for that store, not the store! Hacks should be revisited or not even implemented in the first place! The whole problem here is that the element events are not coupled with an object that retains information about the "event trigger", if you will, and therefore determining the builder associated with the event became a hack. What's needed here is a container for the binder, template ID, etc., that is bound to the specific UI events for that builder - in other words, a view controller.

export class ViewController {
  storeManager: StoreManager;
  parentChildRelationshipStore: ParentChildStore;
  builder: TemplateBuilder;
  eventRouter: EventRouter;
  store: Store;
  childControllers: ViewController[] = [];
  selectedRecordIndex: number = -1; // multiple selection not allowed at the moment.

  constructor(storeManager: StoreManager, 
             parentChildRelationshipStore: ParentChildStore, eventRouter: EventRouter) {
    this.storeManager = storeManager;
    this.parentChildRelationshipStore = parentChildRelationshipStore;
    this.eventRouter = eventRouter;
}

Note a couple things here:

  1. The selected record index is associated with the view controller.
  2. A view controller manages its list of child controllers. This ensures that in scenarios like A-B-C and D-B-C, the controllers for B and C are distinct with regards to the roots A and D.

Now, when a "Create..." button is clicked, the view controller passes in to the store the view controller instance:

jQuery(createButtonId).on('click', () => {
  let idx = this.eventRouter.Route("CreateRecord", this.store, 0, this); // insert at position 0

which has the correct builder and therefore template container for entity that is being created, and while the callback is created only once per store:

if (this.storeManager.HasStore(storeName)) {
  this.store = this.storeManager.GetStore(storeName);
} else {
  this.store = this.storeManager.CreateStore(storeName, storeType);
  this.AssignStoreCallbacks();
}

passing "through" the view controller ensures that the correct template container is used:

private AssignStoreCallbacks(): void {
  this.store.recordCreatedCallback = (idx, record, insert, store, onLoad, viewController) => {

    viewController.CreateRecordView(this.store, idx, insert, onLoad);

    // Don't select the first field when called from Store.Load, as this will select the 
    // first field for every record, leaving the last record selected. Plus we're not
    // necessarily ready to load up child records yet since the necessary view controllers
    // haven't been created.
    if (!onLoad) {
      viewController.FocusOnFirstField(idx);
    }
  };

  this.store.propertyChangedCallback = 
    (idx, field, value) => this.UpdatePropertyView(idx, field, value);
  this.store.recordDeletedCallback = (idx, store, viewController) => {
    // A store can be associated with multiple builders: A-B-C and A-D-C, where the store is C
    viewController.RemoveChildRecordsView(store, idx);
    viewController.parentChildRelationshipStore.DeleteRelationship(store, idx);
    viewController.DeleteRecordView(idx);
  }
}

Now to create the page, we do this instead:

let vcProjects = new ViewController(storeManager, parentChildRelationshipStore, eventRouter);
vcProjects.CreateStoreViewFromTemplate(
  "Projects", 
  StoreType.LocalStorage, 
  "#projectTemplateContainer", 
  projectTemplate, "#createProject", 
  true, 
  undefined, 
  (idx, store) => store.SetDefault(idx, "Status", projectStates[0].text));

new ViewController(storeManager, parentChildRelationshipStore, eventRouter).
  CreateStoreViewFromTemplate(
    "Contacts", 
    StoreType.LocalStorage, 
    "#projectContactTemplateContainer", 
    contactTemplate, 
    "#createProjectContact", 
    false, 
    vcProjects);

etc. Notice how when we create the Contacts view controller, which is a child of Projects, we pass in the parent controller, which registers the child with its parent:

if (parentViewController) {
  parentViewController.RegisterChildController(this);
}

The child collection is used to create and remove views using the correct view controller:

childRecs.childrenIndices.map(idx => Number(idx)).forEach(recIdx => {
  let vc = this.childControllers.find(c => c.store.storeName == child);
  vc.CreateRecordView(childStore, recIdx, false);
});

The global variables are eliminated because they are contained now in the view controller. If at runtime, a new view controller needs to be instantiated, this would be done by the parent view controller and it can pass in singletons such as the store manager and event router, and parent-child relationship store.

Day 13 - Audit Log

Persisting to local storage is not really a viable long-term solution. While it may be useful for off-line work, we need a centralized server for the obvious - so that more than one person can access the data and so that I can access the same data from different machines. This involves a bunch of work:

Image 39

(Oh look, sub-tasks!!!)

Store Persistence Inversion of Control

So far, we have only local storage persistence, so we'll wrap the functions in this class:

export class LocalStoragePersistence implements IStorePersistence {
  public Load(storeName: string): RowRecordMap {
    let json = window.localStorage.getItem(storeName);
    let data = {};

    if (json) {
      try {
        // Create indices that map records to a "key", 
       // in this case simply the initial row number.
        let records: {}[] = JSON.parse(json);
        records.forEach((record, idx) => data[idx] = record);
      } catch (ex) {
        console.log(ex);
        // Storage is corrupt, eek, we're going to remove it!
        window.localStorage.removeItem(storeName);
      }
    }

    return data;
  }

  public Save(storeName: string, data: RowRecordMap): void {
    let rawData = jQuery.map(data, value => value);
    let json = JSON.stringify(rawData);
    window.localStorage.setItem(storeName, json);
  }

  public Update(storeName: string, data:RowRecordMap, record: {}, 
               idx: number, property: string, value: string) : void {
    this.Save(storeName, data);
  }
}

Load, save, and update are then just calls into the abstracted persistence implementation:

public Load(createRecordView: boolean = true, 
           viewController: ViewController = undefined): Store {
  this.data = this.persistence.Load(this.storeName);

  if (createRecordView) {
    jQuery.each(this.data, (k, v) => this.recordCreatedCallback
                                    (k, v, false, this, true, viewController));
  }

  return this;
}

public Save(): Store {
  this.persistence.Save(this.storeName, this.data);

  return this;
}

public UpdatePhysicalStorage(idx: number, property: string, value: string): Store {
  let record = this.data[idx];
  this.persistence.Update(this.storeName, this.data, record, idx, property, value);

  return this;
}

Image 40

Woohoo!

Audit Log

Logging the CRUD operations is actually an audit log, so we might as well call it that. This is a concrete store backed by a concrete model:

export class AuditLogModel {
  storeName: string;
  action: AuditLogAction;
  recordIndex: number;
  property: string;
  value: string;

  constructor(storeName: string, action: AuditLogAction, recordIndex: number, 
             property: string, value: string) {
    this.storeName = storeName;
    this.action = action;
    this.recordIndex = recordIndex;
    this.property = property;
    this.value = value;
  }

  // Here we override the function because we don't want to log the audit log 
 // that calls SetRecord above.
    public SetRecord(idx: number, record: {}): Store {
    this.CreateRecordIfMissing(idx);
    this.data[idx] = record;

    return this;
  }

  // If we don't override this, calling CreateRecord here causes 
 // an infinite loop if the AuditLogStore doesn't exist yet,
  // because when the audit log store asks for its next sequence number, 
 // and the store doesn't exist,
  // SequenceStore.GetNext is called which calls CreateRecord, 
 // recursing into the Log function again.
  protected GetPrimaryKey(): {} {
    return {};
  }
}

where the actions are:

export enum AuditLogAction {
  Create,
  Update,
  Delete
}

Here's the log where I modified the project name, created a contact, then deleted the contact:

Image 41

Here's an example of creating a sequence for an entity (in this case "Links") that doesn't exist yet:

Image 42

This was the result of this code change in the store regarding the function SetRecord, which is why it's overridden in the AuditLogStore.

public SetRecord(idx: number, record: {}): Store {
  this.CreateRecordIfMissing(idx);
  this.data[idx] = record;

  jQuery.each(record, (k, v) => this.auditLogStore.Log
                     (this.storeName, AuditLogAction.Update, idx, k, v)); 

  return this;
}

So this is where we're at now:

Image 43

Day 14 - Server-Side Persistence

I'm implementing the server in .NET Core so I can run it on non-Windows devices as it is really just a proxy for database operations. Plus I'm not going to use EntityFramework or Linq2Sql. And while I considered using a NoSQL database, I wanted the flexibility to create queries on the database that include table joins, and that's sort of a PITA -- not every NoSQL database engine implements the ability and I don't really want to deal with the $lookup syntax in MongoDB that I wrote about here.

Async Client-Side Calls

But we have a bigger problem -- AJAX calls are by nature asynchronous and I've not accounted for any asynchronous behaviors in the TypeScript application. If you were thinking about that while reading this article, you are probably giggling. So for the moment (I haven't decided if I want to make Load async as well), I've modified the store's Load function like this:

public Load(createRecordView: boolean = true, 
 viewController: ViewController = undefined): Store {
  this.persistence.Load(this.storeName).then(data => {
    this.data = data;

    if (createRecordView) {
      jQuery.each(this.data, (k, v) => this.recordCreatedCallback
                                      (k, v, false, this, true, viewController));
    }
  });

  return this;
}

The signature of the function in the IStorePersistence interface has to be modified to:

Load(storeName: string): Promise<RowRecordMap>;

And the LocalStoragePersistence class' Load function now looks like this:

public Load(storeName: string): Promise<RowRecordMap> {
  let json = window.localStorage.getItem(storeName);
  let data = {};

  if (json) {
    try {
      // Create indices that map records to a "key", in this case simply the initial row number.
      let records: {}[] = JSON.parse(json);
      records.forEach((record, idx) => data[idx] = record);
    } catch (ex) {
      console.log(ex);
      // Storage is corrupt, eek, we're going to remove it!
      window.localStorage.removeItem(storeName);
    }
  }

  return new Promise((resolve, reject) => resolve(data));
}

All is well with the world.

The CloudPersistence class then looks like this:

export class CloudPersistence implements IStorePersistence {
  baseUrl: string;

  constructor(url: string) {
    this.baseUrl = url;
  }

  public async Load(storeName: string): Promise<RowRecordMap> {
    let records = await jQuery.ajax({ url: this.Url("Load") + `?StoreName=${storeName}` });
    let data = {};

    // Create indices that map records to a "key", in this case simply the initial row number.
    records.forEach((record, idx) => data[idx] = record);

    return data;
  }

  public Save(storeName: string, data: RowRecordMap): void {
    let rawData = jQuery.map(data, value => value);
    let json = JSON.stringify(rawData);
    jQuery.ajax
     ({ url: this.Url("Save") + `?StoreName=${storeName}`, type: "POST", data: json });
  }

  private Url(path: string): string {
    return this.baseUrl + path;
  }
}

The concern here is that the Save and Update functions with their asynchronous AJAX calls may be not be received in the same order they are sent. This code needs to be refactored to ensure that the Asynchronous JavasScript and XML (AJAX!) is actually performed in the correct order by queuing the requests and processing them serially, waiting for the response from the server before sending the next one. Another day!

Server-Side Handlers

On the server side (I'm not going to go into my server implementation at the moment), I register this route:

router.AddRoute<LoadStore>("GET", "/load", Load, false);

and implement a route handler that returns a dummy empty array:

private static IRouteResponse Load(LoadStore store)
{
  Console.WriteLine($"Load store {store.StoreName}");

  return RouteResponse.OK(new string[] {});
}

Somewhat ironically, I also had to add:

context.Response.AppendHeader("Access-Control-Allow-Origin", "*");

because the TypeScript page is being served by one address (localhost with a port that Visual Studio assigns) and my server is sitting on localhost:80. It's interesting to watch what happens without this header -- the server gets the request but the browser blocks (throws an exception) processing the response. Sigh.

Model-less SQL

Now we get to a decision. Typically the database schema is created as a "known schema", using some sort of model / schema synchronization, or a migrator like FluentMigrator, or just hand-coded. Personally, I have come to loathe this whole approach because it usually means:

  1. The database has a schema that requires management.
  2. The server-side has a model that requires management.
  3. The client-side has a model that also requires management.

My God! What ever happened to the DRY (Don't Repeat Yourself) principle when it comes to schemas and models? So I'm going to conduct an experiment. As you've noticed, there is no real model of anything on the client-side except for the couple concrete types for the audit and sequence "tables." My so-called model is actually hidden in the view templates, for example:

let contactTemplate = [
  { field: "Name", line: 0, width: "30%", control: "textbox" },
  { field: "Email", line: 0, width: "30%", control: "textbox" },
  { field: "Title", line: 0, width: "30%", control: "textbox" },
  { field: "Comment", line: 1, width: "80%", control: "textbox" },
  { text: "Delete", line: 1, width: "80px", control: "button", route: "DeleteRecord" }
];

Oh look, the template for the view specifies the fields in which the view is interested. In the local storage implementation, that was quite sufficient. This would all be fine and dandy in a SQL database if I basically had a table like this:

ID
StoreName
PropertyName
Value

Rant on. But I don't want that -- I want concrete tables with concrete columns! So I'm going to do something you are going to kick and scream about - create the tables and necessary columns on the fly, as required, so that the view templates are the "master" for defining the schema. Yes, you read that correctly. Just because the whole world programs in a way that duplicates the schema, code-behind model, and client-side model, doesn't mean I have to. Sure there's a performance hit, but we're not dealing with bulk updates here, we're dealing with asynchronous user-driven updates. The user is never going to notice and more importantly to me, I will never again have to write migrations or create tables and schemas or create C# classes that mirror the DB schema. Unless I'm doing some specific business logic on the server side, in which case the C# classes can be generated from the database schema. There was some work in F# ages ago that I encountered where the DB schema could be used to tie in Intellisense to F# objects, but sadly that has never happened in C#, and using dynamic objects has a horrid performance and no Intellisense. So, there is still a major disconnect in programming language support that "knows" the DB schema. Rant off.

Tomorrow.

Day 15 - Creating the Schema on the Fly

Before getting into this, one minor detail is needed - a user ID that is associated with the AJAX calls so data can be separated by user. For testing, we'll use:

let userID = new Guid("00000000-0000-0000-0000-000000000000");
let persistence = new CloudPersistence("http://127.0.0.1/", userId);

There is no login or authentication right now, but it's useful to put this into the coding now rather than later.

So now, our cloud persistence Load function looks like this:

public async Load(storeName: string): Promise<RowRecordMap> {
  let records = await jQuery.ajax({
    url: this.Url("Load") + 
        this.AddParams({ StoreName: storeName, UserId: this.userId.ToString() }) });
  let data = {};

  // Create indices that map records to a "key", in this case simply the initial row number.
  // Note how we get the record index from record.__ID!!!
  records.forEach((record, _) => data[record.__ID] = record);

  return data;
}

Send the Audit Log

The Save function sends the current state of the audit log:

public Save(storeName: string, data: RowRecordMap): void {
  // For cloud persistence, what we actually want to do here is 
 // send over the audit log, not the entire store contents.
  let rawData = this.auditLogStore.GetRawData();
  let json = JSON.stringify(rawData);
  jQuery.post(this.Url("Save") + 
   this.AddParams({ UserId: this.userId.ToString() }), JSON.stringify({ auditLog: json }));
  this.auditLogStore.Clear();
}

Note how the log is cleared once we have sent it!

Save the Audit Log

A special function is required to actually send the audit log itself because it is not in the form "action-property-value", it is a concrete entity:

public SaveAuditLog(logEntry: AuditLogModel): void {
  let json = JSON.stringify(logEntry);
  jQuery.post(this.Url("SaveLogEntry") + 
             this.AddParams({ UserId: this.userId.ToString() }), json);
}

Load the Current Schema

On the server side, we load what we know about the schema:

private static void LoadSchema()
{
  const string sqlGetTables = 
   "SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE'";
  using (var conn = OpenConnection())
  {
    var dt = Query(conn, sqlGetTables);

    foreach (DataRow row in dt.Rows)
    {
      var tableName = row["TABLE_NAME"].ToString();
      schema[tableName] = new List<string>();
      var fields = LoadTableSchema(conn, tableName);
      schema[tableName].AddRange(fields);
    }
  }
}

private static IEnumerable<string> LoadTableSchema(SqlConnection conn, string tableName)
{
  string sqlGetTableFields = 
   $"SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @tableName";
  var dt = Query(conn, sqlGetTableFields, 
          new SqlParameter[] { new SqlParameter("@tableName", tableName) });
  var fields = (dt.AsEnumerable().Select(r => r[0].ToString()));

  return fields;
}

Create Stores (Tables) and Columns on the Fly

Then we have to create the stores on the fly as needed:

private static void CheckForTable(SqlConnection conn, string storeName)
{
  if (!schema.ContainsKey(storeName))
  {
    CreateTable(conn, storeName);
    schema[storeName] = new List<string>();
  }
}

private static void CheckForField(SqlConnection conn, string storeName, string fieldName)
{
  if (!schema[storeName].Contains(fieldName))
  {
    CreateField(conn, storeName, fieldName);
    schema[storeName].Add(fieldName);
  }
}

private static void CreateTable(SqlConnection conn, string storeName)
{
  // __ID must be a string because in ParentChildStore.GetChildInfo, 
  // this Javascript: childRecIds.indexOf((<any>r).__ID)
  // Does not match on "1" == 1
  string sql = $"CREATE TABLE [{storeName}] (ID int NOT NULL PRIMARY KEY IDENTITY(1,1), 
              UserId UNIQUEIDENTIFIER NOT NULL, __ID nvarchar(16) NOT NULL)";
  Execute(conn, sql);
}

private static void CreateField(SqlConnection conn, string storeName, string fieldName)
{
  // Here we suffer from a loss of fidelity 
 // as we don't know the field type nor length/precision.
  string sql = $"ALTER TABLE [{storeName}] ADD [{fieldName}] NVARCHAR(255) NULL";
  Execute(conn, sql);
}

Save the Audit Log

And finally, we process the audit log on save:

private static IRouteResponse Save(SaveStore store)
{
  var logs = JsonConvert.DeserializeObject<List<AuditLog>>(store.AuditLog);

  using (var conn = OpenConnection())
  {
    // Evil!
    lock (schemaLocker)
    {
      UpdateSchema(conn, logs);

      // The CRUD operations have to be in the lock operation 
     // so that another request doesn't update the schema while we're updating the record.
      logs.ForEach(l => PersistTransaction(conn, l, store.UserId));
    }
  }

  return RouteResponse.OK();
}

private static void PersistTransaction(SqlConnection conn, AuditLog log, Guid userId)
{
  switch (log.Action)
  {
    case AuditLog.AuditLogAction.Create:
      CreateRecord(conn, userId, log.StoreName, log.RecordIndex);
      break;

    case AuditLog.AuditLogAction.Delete:
      DeleteRecord(conn, userId, log.StoreName, log.RecordIndex);
      break;

    case AuditLog.AuditLogAction.Update:
      UpdateRecord(conn, userId, log.StoreName, log.RecordIndex, log.Property, log.Value);
      break;
  }
}

Update the Schema on the Fly

Notice the call to UpdateSchema! This is where the magic happens, that if a field in the table hasn't been encountered before, we create it on the fly!

private static void UpdateSchema(SqlConnection conn, List<AuditLog> logs)
{
  // Create any missing tables.
  logs.Select(l => l.StoreName).Distinct().ForEach(sn => CheckForTable(conn, sn));

  // Create any missing fields.
  foreach (var log in logs.Where
         (l => !String.IsNullOrEmpty(l.Property)).DistinctBy(l => l, tableFieldComparer))
  {
    CheckForField(conn, log.StoreName, log.Property);
  }
}

Et voilà!

Image 44

At this point, I haven't entered anything for the TODO and Description fields, so the schema doesn't know they exist:

Image 45

After I fill in the data:

Image 46

The schema has been modified because these additional columns were part of the audit log!

Image 47

And we can see the audit log entries logged as well for the changes I just made:

Image 48

And all the tables that were created on the fly (except for the AuditLogStore table):

Image 49

Day 16 - More Bugs

Bug in How Entity __ID is Working

After a page refresh, I discovered that the sequencer was creating the next number (let's say we're at a count of 2) as "21", then "211", then "2111". This is a problem with the fact that there is no type information, so on a page refresh, the "number" was coming in as a string and this line of code:

n = this.GetProperty(recIdx, "count") + 1;

ended up appending the character 1, not incrementing the count. As long as I didn't refresh the page in my testing, everything worked fine. Refresh the page and new parent-child relationships stopped working! The workaround, lacking type information to serialize the count as a number in JSON rather than as a string, is:

// Number because this field is being created in the DB 
// as an nvarchar since we don't have field types yet!
n = Number(this.GetProperty(recIdx, "count")) + 1;

The next problem was that the audit log wasn't passing the correct client-side "primary key" (the __ID field), which occurred after deleting records. This code:

public Log(storeName: string, action: AuditLogAction, 
          recordIndex: number, property?: string, value?: any): void {
  let recIdx = this.InternalCreateRecord(); // no audit log for the audit log!
  let log = new AuditLogModel(storeName, action, recIdx, property, value);

worked fine as long as the record index (the indexer into the store's data) was in sync with the sequence counter. When they became out of sync, after deleting records and doing a page refresh, again the new entities being created were saved with an __ID starting at 1 again! The sequence count was ignored. The fix was to get the client-side __ID, as this is the primary key to the record on the server, which is not the primary key if the table:

public Log(storeName: string, action: AuditLogAction, 
          recordIndex: number, property?: string, value?: any): void {
  let recIdx = this.InternalCreateRecord(); // no audit log for the audit log!
  let id = this.storeManager.GetStore(storeName).GetProperty(recordIndex, "__ID");
  let log = new AuditLogModel(storeName, action, id, property, value);

After making that change, persisting changes to the sequencer stopped working because it didn't even have an __ID, so my thinking was wrong there -- it definitely needs and __ID so that the SetRecord function works and after creating a relationship, the appropriate fields in the parent-child store get updated correctly:

public SetRecord(idx: number, record: {}): Store {
  this.CreateRecordIfMissing(idx);
  this.data[idx] = record;
  jQuery.each(record, (k, v) => this.auditLogStore.Log
            (this.storeName, AuditLogAction.Update, idx, k, v));

  return this;
}

The fix involved changing this override in the SequenceStore:

protected GetPrimaryKey(): {} {
  return {};
}

to this:

// Sequence store has to override this function so that we don't recursively call GetNext
// when CreateRecord is called above. 
// We need __ID so the server knows what record to operate on.
protected GetNextPrimaryKey(): {} {
  let id = Object.keys(this.data).length;
  return { __ID: id };
}

Good grief. That was not amusing.

Revisiting this mess:

private static void CreateTable(SqlConnection conn, string storeName)
{
  // __ID must be a string because in ParentChildStore.GetChildInfo, 
 // this Javascript: childRecIds.indexOf((<any>r).__ID)
  // Does not match on "1" == 1
  string sql = $"CREATE TABLE [{storeName}] (ID int NOT NULL PRIMARY KEY IDENTITY(1,1), 
              UserId UNIQUEIDENTIFIER NOT NULL, __ID nvarchar(16) NOT NULL)";
  Execute(conn, sql);
}

It would probably behoove me to create a concrete model for the ParentChildRelationships store as right now it's being created on the fly and lacking type information, the parentId and childId fields are being created in nvarchar:

Image 50

I can certainly appreciate the need to have an actual model definition for each server-side table and client-side usage, but I really don't want to go down that route! However, it would actually be useful to create an index on the (UserId, __ID) field pair as the update and delete operations always use this pair to identify the record:

private static void CreateTable(SqlConnection conn, string storeName)
{
  // __ID must be a string because in ParentChildStore.GetChildInfo, 
 // this Javascript: childRecIds.indexOf((<any>r).__ID)
  // Does not match on "1" == 1
  string sql = $"CREATE TABLE [{storeName}] (ID int NOT NULL PRIMARY KEY IDENTITY(1,1), 
              UserId UNIQUEIDENTIFIER NOT NULL, __ID nvarchar(16) NOT NULL)";
  Execute(conn, sql);
  string sqlIndex = $"CREATE UNIQUE INDEX [{storeName}Index] ON [{storeName}] (UserId, __ID)";
  Execute(conn, sqlIndex);
}

Forgot to Register the Common Fields Bug

Another bug surfaced which I missed in the console log -- when creating a table, the in-memory schema on the server side wasn't updating the fields UserId and __ID after creating the table. The fix was straight forward, though I don't like the decoupling between the call to CreateTable and adding in the two fields that CreateTable creates:

private static void CheckForTable(SqlConnection conn, string storeName)
{
  if (!schema.ContainsKey(storeName))
  {
    CreateTable(conn, storeName);
    schema[storeName] = new List<string>();
    schema[storeName].AddRange(new string[] { "UserId", "__ID" });
  }
}

I probably didn't notice this for ages because I hadn't dropped all the tables to create a clean slate in quite a while, at least until I modified the code above to create the indexes! Sigh. I really need to create unit tests.

Bonus Day 17 - Entity Menu Bar

Originally, I wanted a side-menu bar that would determine what child entities were visible. While this still seemed like a good idea, I really wasn't sure how it would work. I did know one thing though -- the screen gets quite cluttered with a lot of projects and the views for the children and sub-children, which now includes:

  • Project Bugs
  • Project Contacts
  • Project Notes
  • Project Links
  • Project Tasks
  • Task Notes
  • Task Links
  • Sub-Tasks

Not only is the screen cluttered but it's also difficult to see what project is selected, and as the project list grows bigger, vertical scrolling will take place which is an added annoyance to seeing the children of a project and potentially their grandchildren, etc. What I needed was a way to focus on a specific project and then de-focus when switching projects. And I wanted it to be easy to focus and de-focus the project without adding additional buttons like "Show Project Details" and "Back to Project List", or some such silliness, especially since this would cascade for children of children, like "Show Task Details" and "Back to Tasks." So after staring at the UI for a good hour in contemplation (I kid you not, though I did have an interesting conversation at the Farm Store during this time with a total stranger, and I was at the Farm Store because the winds had created an 8 hour power outage on Friday, and did you really read this and did you really click on the Hawthorne Valley Farm Store link?) I opted for the following behavior:

  • Clicking on any control of a specific entity's record will hide all other sibling entities. This removes all siblings so I know exactly what entity I'm working with, and workings regardless of where I am in the entity hierarchy.
  • Clicking on the first control (which I would think is almost always an edit box but that remains to be seen) de-selects that entity and shows all siblings again. (Deleting an entity will do the same thing.)
  • Now, here's the fun part -- depending on what entities you've selected in the menu bar, only those children are shown when you "focus" on a parent entity.
  • De-selecting the focused entity will hide child entities that have been selected in the menu bar.

To illustrate, here's a sample project list (really original naming here):

Image 51

Click on an entity (such as "01 P") and you see:

Image 52

That's it! The siblings have been hidden. Click on the first control, in this case the edit box containing the text "01 P", and it becomes de-selected and all the siblings are shown again. As stated above, this works anywhere in the hierarchy.

Now here's the entity menu bar:

Image 53

I'll clicking on Tasks in the menu bar and, assuming "01 P" is selected, I get its tasks:

Image 54

Now I'll also select "Sub-Tasks":

Image 55

Notice the "Create Sub-Task" button, which is actually a bug because I shouldn't be able to create a child without a parent being selected. But regardless, notice that I haven't selected a task. As soon as I select a task, its sub-tasks appear:

Image 56

I'm finding this UI behavior quite comfortable:

  • I can select just the entity I want to work with.
  • I can select just the child entities I want to see in the selected entity.
  • I can easily de-select seeing the child entities.
  • I can easily go back to seeing the entire list of siblings.
  • I can easily see what entities in the hierarchy I've selected to see when I select the parent entity.

To accomplish all this, in the HTML I added:

<div class="row menuBar">
  <div id="menuBar">
  </div>
</div>
  <div class="row entityView">
  ...etc...

and in the application initialization:

let menuBar = [
  { displayName: "Bugs", viewController: vcProjectBugs },
  { displayName: "Contacts", viewController: vcProjectContacts },
  { displayName: "Project Notes", viewController: vcProjectNotes },
  { displayName: "Project Links", viewController: vcProjectLinks },
  { displayName: "Tasks", viewController: vcProjectTasks },
  { displayName: "Task Notes", viewController: vcProjectTaskNotes },
  { displayName: "Task Links", viewController: vcProjectTaskLinks },
  { displayName: "Sub-Tasks", viewController: vcSubtasks }
];

let menuBarView = new MenuBarViewController(menuBar, eventRouter);
menuBarView.DisplayMenuBar("#menuBar");

The menu bar and menu items are defined in TypeScript as:

import { MenuBarItem } from "./MenuBarItem"

export interface MenuBar extends Array<MenuBarItem> { }

and:

import { ViewController } from "../classes/ViewController"

export interface MenuBarItem {
  displayName: string;
  viewController: ViewController;
  id?: string;                // used internally, never set
  selected?: boolean;         // used internally, never set
}

The more interesting part of this is how MenuBarViewController interacts with the ViewController -- I really should rename that to be the EntityViewController! Notice in the constructor a couple event routes being defined:

export class MenuBarViewController {
  private menuBar: MenuBar;
  private eventRouter: EventRouter;

  constructor(menuBar: MenuBar, eventRouter: EventRouter) {
    this.menuBar = menuBar;
    this.eventRouter = eventRouter;

    this.eventRouter.AddRoute("MenuBarShowSections", 
                    (_, __, vc:ViewController) => this.ShowSections(vc));
    this.eventRouter.AddRoute("MenuBarHideSections", 
                    (_, __, vc: ViewController) => this.HideSections(vc));
}

The two key handlers are:

private ShowSections(vc: ViewController): void {
  vc.childControllers.forEach(vcChild => {
    this.menuBar.forEach(item => {
      if (item.selected && vcChild == item.viewController) {
        item.viewController.ShowView();
      }
    });

    this.ShowSections(vcChild);
  });
}

private HideSections(vc: ViewController): void {
  vc.childControllers.forEach(vcChild => {
    this.menuBar.forEach(item => {
      if (item.selected && vcChild == item.viewController) {
        item.viewController.HideView();
      }
    });

    this.HideSections(vcChild);
  });
}

Now, in the entity view controller, I changed jel.on('focus', (e) => { to: jel.on('click', (e) => for when the user focuses/clicks on an entity's control. Clicking on an entity's control has the added behavior now of showing and hiding siblings as well as child entities based on the menu bar selection:

if (this.selectedRecordIndex != recIdx) {
  this.RemoveChildRecordsView(this.store, this.selectedRecordIndex);
  this.RecordSelected(recIdx);
  this.selectedRecordIndex = recIdx;
  this.ShowChildRecords(this.store, recIdx);

  this.HideSiblingsOf(templateContainer);
  // Show selected child containers as selected by the menubar
  this.eventRouter.Route("MenuBarShowSections", undefined, undefined, this);
} else {
  let firstElement = jQuery(e.currentTarget).parent()[0] == 
                    jQuery(e.currentTarget).parent().parent().children()[0];

  if (firstElement) {
    // If user clicks on the first element of selected record,
    // the deselect the record, show all siblings, and hide all child records.
    this.ShowSiblingsOf(templateContainer);
    this.RemoveChildRecordsView(this.store, this.selectedRecordIndex);
    this.RecordUnselected(recIdx);
    this.selectedRecordIndex = -1;
    // Hide selected child containers as selected by the menubar
    this.eventRouter.Route("MenuBarHideSections", undefined, undefined, this);
  }
}

And that was it!

Running the Application

If you want to run the application using local storage, in AppMain.js, make sure the code reads:

let persistence = new LocalStoragePersistence();
// let persistence = new CloudPersistence("http://127.0.0.1/", userId);

If you want to run the application using a database:

  1. Create a database called TaskTracker. Yeah, that's it, you don't have to define any of the tables, they are created for you.
  2. In the server application, Program.cs, set up your connection string: private static string connectionString = "[your connection string]";
  3. Open a command window "as administrator" and cd to the root of the server application, then type "run". This builds .NET Core application and launches the server.
  4. To exit the server, press Ctrl+C (I have a bug shutting down the server!)
  5. If you need to change the IP address or port, do so in the TypeScript (see above) and in the server application.

And enable the cloud persistence:

// let persistence = new LocalStoragePersistence();
let persistence = new CloudPersistence("http://127.0.0.1/", userId);

Conclusion

So this article is huge. You should probably read it one day at a time! And it's also crazy -- this is metadata driven, view defines the model, schema generated on the fly, bizarre approach to building an application. There's a lot to do still to make this even more interesting such as storing the template view definitions and HTML in the database specific to the user, giving the user the flexibility to customize the entire presentation. The UI is ugly as sin, but it actually does the job quite nicely for what I wanted to accomplish -- organizing projects, tasks, contacts, links, bugs, and notes in a way that is actually useful to, well, me! Other serious warts exist, such as all fields are created as nvarchar since we don't have type information!

I hope you had fun reading this, maybe some of the ideas here are interesting if not jarring, and I'll expect to follow up with some more interesting features in the future, such as synchronizing the local store with the cloud store, which really is broken right now because the audit trail is cleared whenever a "store save" is done. Oops! Another thing I want to take a look at is the fact that I'm loading all the user's "store" data on client startup - it would be more interesting to load only the child data relevant to the selected project. Basically, a mechanism to say "if I don't have these records, get them now."

Lastly, if you're interested in watching how this project develops, I'll be posting updates to the repo on GitHub.

Well, anyways, that's all for now folks!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Marc Clifton
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Pete O'Hanlon27-Nov-19 0:27
communityengineerPete O'Hanlon27-Nov-19 0:27 
GeneralMy vote of 5 Pin
Degryse Kris19-Nov-19 0:46
MemberDegryse Kris19-Nov-19 0:46 
QuestionMy apologies Marc Pin
Pete O'Hanlon4-Nov-19 22:25
communityengineerPete O'Hanlon4-Nov-19 22:25 
AnswerRe: My apologies Marc Pin
Marc Clifton5-Nov-19 6:46
mvaMarc Clifton5-Nov-19 6:46 
GeneralRe: My apologies Marc Pin
Pete O'Hanlon6-Nov-19 1:41
communityengineerPete O'Hanlon6-Nov-19 1:41 
GeneralRe: My apologies Marc Pin
Marc Clifton10-Nov-19 12:26
mvaMarc Clifton10-Nov-19 12:26 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Article
Posted 2 Nov 2019

Stats

5.6K views
118 downloads
16 bookmarked