[译]16天:从概念到实现的TypeScript应用程序
By robot-v1.0
本文链接 https://www.kyfws.com/applications/16-days-a-typescript-application-from-concept-to-zh/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 76 分钟阅读 - 37619 个词 阅读量 0[译]16天:从概念到实现的TypeScript应用程序
原文地址:https://www.codeproject.com/Articles/5250141/16-Days-A-TypeScript-Application-from-Concept-to
原文作者:Marc Clifton
译文由本站 robot-v1.0 翻译
前言
A metadata driven, view defines the model, schema generated on the fly, from concept to prototype application in 16 days
由元数据驱动的视图定义了模型,从概念到原型应用程序在16天内即时生成的架构
此屏幕快照只是此处已实现的一部分.右侧是与项目和任务关联的其他链接:(This screenshot is only a subset of what’s been implemented here. On the right are additional links associated with projects and tasks:)
目录(Table Of Contents)
-
第2天-元数据的一些结构概念(Day 2 - Some Structure Concepts for the Metadata)
-
第7天-序列存储和亲子关系存储(Day 7 - Sequence Store and the Parent-Child Relationship Store)
-
-
错误:将生成器与正确的父子上下文关联(Bug: Associate the Builder with the correct Parent-Child Context)
-
错误:将CRUD操作与正确的构建器上下文相关联(Bug: Associate the CRUD Operations with the Correct Builder Context)
-
错误:所选记录取决于父子关系(Bug: The Selected Record is Parent-Child Dependent)
-
尼斯:添加记录时将重点放在第一场(Nicety: Focus on First Field when Adding a Record)
介绍(Introduction)
我认为记录从概念到实现的客户端TypeScript应用程序的创建将是一件有趣且充满希望的事情.因此,我选择了一段时间以来一直想做的事情-专为我非常特定的需求量身定制的项目任务管理器.但我也希望此实现高度抽象,这意味着用于UI布局和父子实体关系的元数据.换句话说,在一天结束时,(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(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>
在客户端上完成容器内容创建工作的地方.因此,本文介绍的是一种创建通用的父子实体编辑器这样的实现的方法,但是要在特定的项目任务管理器的上下文中进行,您将看到从概念到工作应用程序的演变,放置超过15天.(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:)
- 除具体模型外,整个"模型"概念都被抛在了窗外.视图定义了模型!(Except for concrete models, there entire “model” concept is thrown out the window. The view defines the model!)
- 根据需要即时生成表和列.对.(Table and column generation as needed on the fly. Yup.) 顺便说一下,日子不是连续的-虽然每天都记录我所做的工作,但这并不意味着我每天都在做这项工作.特别是第12天的物理时间跨度为3天(而不是8小时)!此外,您还应该意识到每天都在更新文章本身!(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!)
此外,是的,可以使用网格来实现,但是默认的HTML网格功能非常糟糕,我不想为这些文章引入其他第三方库.我最喜欢的是jqWidgets,几乎没人会做(即使它很大),所以也许在某个时候,我将演示如何将所有这些东西绑定到它们的库中.(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:)
- 通过查看布局可以清楚地看到,它实际上更多地是满足我自己的需求的模板,并且左侧的实际"任务栏"以及每个任务项的特定字段应完全由用户定义,内容和控制权.(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.)
- 这几乎意味着我们正在研究一种NoSQL数据库结构,该结构在任务和任务项之间存在松散的链接.一切的"根"仍然是任务,但任务项及其字段实际上是任意的.(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.)
- 某些字段最终以离散形式表示的数组(如URL链接),而其他字段(如注释)则可能离散地显示为不同textarea条目的可滚动集合,或更多地显示为"文档",其中用户只需滚动单个文本区域.(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?)
- 就个人而言,我更喜欢客户端.服务器应最少参与布局-服务器应按数据而不是布局提供内容.这种方法还无需服务器即可促进UI的开发,并将所有UI代码保留在客户端上,而不是将其分布在后端的JavaScript和C#上.不,我对在后端使用node.js不感兴趣.(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.)
第2天-元数据的一些结构概念(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))
- 等待3(Waiting for 3)rd(rd)派对(Party)
- 等待同事(Waiting for Coworker)
- 等待管理(Waiting on Management)
- 卡住(Stuck) 请注意,我在任务旁边没有优先级.我真的不关心优先事项-通常会有很多事情在进行,而我会根据自己的心情和可以做的事情进行工作.当然,如果您喜欢优先级,可以将它们添加到UI.(Notice that I don’t have a priority next to the task. I really don’t give a sht 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.*)
请注意,我也没有将任务分类为例如sprint,平台,客户等.同样,如果需要这些东西,可以添加它们.(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:)
- 任务是什么?(What is the task?)
- 它的状态是什么?(What is its state?)
- 一行描述为什么它处于这种状态.(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):)
我们如何在JSON中定义此布局,以便您可以创建需要的内容?几乎,这意味着首先要弄清楚如何满足我的需求!(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)
第3天-模板(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) 文章.因此,允许的关系也应该定义也是很自然的.但是,在这一点上,我更愿意做一些原型制作,以了解其中的一些想法是如何实现的.因此,让我们从上面的JSON开始,编写一个将其转换为HTML模板的函数,然后可以根据需要重复应用该模板.同时,我将学习TypeScript的细微差别!(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:)
由模板数组定义:(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
整理HTML的类:(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:)
凉.可能不是最漂亮的东西,但我正在寻找基本知识.(Cool. May not be the prettiest thing, but the basics are what I’m looking for.)
现在,对我个人而言,无休止的是,模板对象使我想起了ExtJ:基本上是定义UI布局的任意键的集合.也许这是不可避免的,而且我当然不会沿用ExtJs创建每次刷新页面时都会更改的自定义ID的路线.谈论杀死在UI级别执行测试自动化的功能.具有讽刺意味的是,在编写类似这样的内容时,我实际上开始对ExtJ做出的设计决策有了更好的理解.(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) combobox
es实际上是填充的.是的,在ExtJs中有一个"商店"的概念,并且自动操作商店(或者说是理论)会更新UI.现在对我来说这太多了,但是我确实希望能够使用现有对象或从REST调用中获取(并可能缓存)该对象.因此,让我们将一些简单的事情放在一起.这是我的状态:(es 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:)
那很容易.(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
.这似乎也很复杂,因为我已经在处理一系列对象!再一次,我意识到为什么在ExtJS商店中总是存储事物数组,即使商店代表单身人士也是如此-因为它更容易!所以我们重构一下(. 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);
这样我们就不会盲目地复制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:)
叹.请注意,生成的HTML具有(Sigh. Note that the resulting HTML has the) storeIdx
所有小写字母的属性.这似乎是jQuery的事情,我将在以后进行调查.接下来,我们需要创建(attribute as all lowercase. This seems to be a jQuery thing that I’ll investigate later. Next, we need to create) onchange
值更改时用于更新商店的处理程序.这必须通过"后期绑定"完成,因为HTML是从模板动态创建的.再次,我明白了为什么ExtJS最终会为元素分配任意ID-我们如何确定绑定对象的元素(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
处理程序?就个人而言,我更喜欢使用单独的属性来唯一标识绑定点,并且可能使用GUID作为属性值.谁知道如果必须绑定数百个元素会对性能产生什么影响,但是老实说,我不会为此担心!(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!)
现在是晚上10:30,我叫它一个晚上!(It’s 10:30 PM, I’m calling it a night!)
第4天-后期装订(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);
有多个具有相同GUID的元素,因为我们已经将HTML模板克隆了三遍.可能不太理想,但我暂时会接受.同时,我为任务创建的商店:(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
!要通过重构来使事物的名称更清晰.因此从现在开始,这就是它们的名字.当使用C#代码时,PITA应该没有"重命名"功能!(! 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:)
我们可以看到这些已被序列化到本地存储(在Chrome中检查本地存储):(we can see that these have been serialized to the local storage (inspecting local storage in Chrome):)
很酷,但是请注意,记录0没有状态,因为我没有更改默认值.怎么办呢?这不是一个容易的问题,因为我们在创建的模板实例数与商店数据之间没有联系.因此,我们需要一种机制来处理该问题并设置默认值.最简单的答案是立即进行暴力破解.至少它是明确的:(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:)
最终,这只会将问题推入"忽略的"存储桶,因为它还取决于状态数组的顺序.但是无论如何,让我们继续前进,现在商店中有了东西,让我们用商店数据加载UI!我们还有一个问题,是应该在每次按键时更新商店,还是仅在每次点击时更新商店?(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;
}
由于在本地存储的情况下更新物理存储会消除我们已保存的所有信息!我已经创建了一个难题-如果记录在本地存储中不存在,我想设置默认值,但是如果它们确实存在,则我不想设置默认值!因此,首先,让我们摆脱副作用,并将物理存储的更新移至onchange处理程序:(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;
}
但是,这会产生令人讨厌的效果,即使没有任何更改,也可能会进行REST调用以保存每个记录.另一个"暂时忽略此问题"问题,但我们肯定需要实现” fielddirty"标志!对于本地存储,我们别无选择,必须保存整个结构,所以现在我们可以开始了.如果没有本地存储,我们将获得所需的默认值:(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:)
并且当有数据时,刷新页面不会使数据消失:(And when there is data, it’s not obliterated by refreshing the page:)
当然,UI不会更新,因为我们还需要绑定才能以其他方式工作!暴力实施如下所示:(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:)
不错,我现在可以创建和保存三个任务!调用它在第4天退出,很快就可以进行反向绑定和更好地处理默认值,并摆脱了这个愚蠢的"三项任务",使任务更具动态性.(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.)
第5天-商店回调(Day 5 - Store Callbacks)
因此,上面的那种蛮力方法需要解决,但是我不希望商店对记录字段如何映射到UI元素一无所知,所以我想我想做的是为记录和属性级别提供回调使用良好的控制反转原则进行更新.可能还应该针对不同的商店类型执行类似的操作,以便应用程序可以覆盖每个商店的行为.后来.(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);
}
现在,我们可以为商店中的记录设置属性,并将其反映在UI中:(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)}`);
因此,让我们看一下添加和删除任务.你们中有些人在笑或吟,是因为我使用"记录索引"概念将自己推到了另一个角落,这使删除和插入任务成为了一场噩梦,因为(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
将与其管理的记录不同步.因此,是时候抛弃整个概念,而采用一种更聪明的方式处理记录了.目前,我已将商店的数据声明为name:value对的数组:(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: {}[] = [];
但是现在是时候变得更聪明了–一种无需使用行索引即可唯一标识记录的方法,以及一种获取与UI元素相关联的唯一标识符的方法.具有讽刺意味的是,数字索引是实现此目的的一种好方法,我们只需要将索引映射到物理记录即可,而不是假定1:1的相关性.我们也不再需要(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));
几乎没有其他更改,因为现在索引不是数组索引,而是字典键,因此以相同的方式使用.在这里,我们假设在初始加载时,记录索引(从0到n-1)与模板构建器创建的索引对应1:1.另一个重要的更改是,要保存到本地存储,我们不想保存key:value模型,而只需保存值,因为键(行索引查找)完全是任意的:(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
因此我们可以将其删除.目前,HTML如下所示:(so we can remove it. Currently, the HTML looks like this:)
其中红色框是一个模板实例.相反,我们想要这样做(使这项工作变得很简单的代码更改,所以我不再展示):(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):)
现在,让我们减小"(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:)
时髦现在,我们必须为活动加油!嗯,好的,这将如何工作?首先,我们需要连接click事件:(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:)
事件路由器(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
调用不知道如何发送REST调用以删除特定记录.(call doesn’t have a clue as to how to send a REST call to delete the specific record.) 我们可以继续前进,然后单击第二项任务T2的删除按钮,我们现在看到:(We can mosey on along and after clicking on the delete button for second task, T2, we now see:)
我们的本地存储如下所示:(and our local storage looks like this:)
现在让我们重构(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)
而且由于在测试中,我淘汰了所有任务,因此现在必须实现"创建任务"按钮!每次创建任务时,模板中所有元素的事件也都需要连接!首先,HTML:(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;
}
请注意,我们如何获取"唯一"记录” index”,以及如何指定是在数据记录的开头还是末尾插入,而不是数据记录(这些记录与顺序无关),而是将标志传递给"视图"来处理应在何处创建模板,因此我们再次重构(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
不默认为” TODO”-必须找出原因.无论如何,从空白开始:(doesn’t default to “TODO” - will have to figure out why. Regardless, starting from a blank slate:)
我创建了两个任务,请注意它们的顺序相反,因为任务是(I created two tasks, note how they are in reverse order because tasks are)*前置的(prepended)*在用户界面中:(in the UI:)
我们可以看到他们是(and we can see that they are)*附加的(appended)*在本地存储中:(in the local storage:)
当然,这会在刷新页面时引起问题:(This, of course, causes a problem when the page is refreshed:)
订单改变了!嗯…(The order got changed! Hmmm…)
现在,从我所见过的有关Vue和其他框架的演示中,在Vue中花了30分钟的时间来完成花5天才能完成的任务.但是,这里的要点是,我实际上是在一起构建框架和应用程序,而且坦率地说,这样做很有趣!这就是全部!第5天结束时,我终于可以创建,编辑和删除任务了!(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);
}
另外,store事件回调是通用的,因此我们可以这样做:(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();
}
但是,当然,这假定UI将具有唯一的容器名称.这使我们进入定义布局的HTML-模板必须位于容器中:(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:)
而且它们在本地存储中的持久性也很好:(and they persist quite nicely in the local storage as well:)
接下来要弄清楚:(To figure out next:)
- 记录中保留的某些唯一ID字段.通常,这是主键,但是我们不会将数据保存到数据库中,我希望将唯一ID与数据库的PK分离,尤其是如果用户正在与Internet断开连接时,我们应该这样做能够相当轻松地支持.(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.)
- 单击父项(在本例中为任务)应调出特定的子记录.(Clicking on the parent (the task in our case) should bring up the specific child records.)
- 我们是否有单独的商店(例如"(Do we have separate stores (like “)
Task-Note
“和”(” and “)Task-Contact
“)对于每个亲子关系,还是我们创建一个”(") for each parent-child relationship or do we create a “)metastore
“具有父子实体名称和唯一ID的实体?还是我们创建一个层次结构,例如,任务中包含诸如注释之类的子元素?(” 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?) - 我们如何向用户指示将与子实体相关联的选定父代?(How do we indicate to the user the selected parent that will be associated with the child entities?) 关于#4,我喜欢这种不引人注目的方法,其中绿色的左边框表示已选择的记录.(Regard #4, I like an unobtrusive approach like this, where the green left border indicates the record that’s been selected.)
这里的技巧是我们只想删除与选择相关联的实体记录的选择:(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:)
关于#3,层次结构是不可能的,因为它可能会导致高度非规范化(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;
}
请注意,父ID和子ID是数字.最多2个(Notice that the parent and child IDs are numbers. The maximum number is 2)1024(1024),但问题是(, the problem though is that the) Number
type是一个64位浮点值,因此它不是范围,而是精度.我猜想通过数字ID而不是GUID ID查找父子关系会更快,并且我现在不必太担心精度.(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.)
而且(恐怖),类似于ExtJS,我们实际上有一个具体的(And (horrors), similar to ExtJS, we actually have a concrete) ParentChildStore
它将具有获取唯一编号ID的功能:(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;
}
在C#中,我会写类似(In C#, I would write something like) GetStore<T>(string storeName) where T : Store
和垂头丧气(and the downcast to) `` 可以正常工作,但是我不知道如何在TypeScript中执行此操作.(would work fine, but I have no idea how to do this in TypeScript.)
虽然我需要一个持久性计数器(如序列)来获取下一个ID,但让我们看一下(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;
}
这是需要设置ID的空对象的分配,但是我不想在商店中编写代码-我更喜欢将其解耦,因此我将其实现为对(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!)
因此,这是第6天的结束.我仍然需要坚持该顺序,可能是”(So this is the end of Day 6. I still need to persist the sequence, probably a “) Sequence
“商店,我可以定义不同的顺序,当然还可以创建父子记录和UI行为.到那里!(” store that allows me to define different sequences, and of course, create the parent-child records and the UI behavior. Getting there!)
第7天-序列存储和亲子关系存储(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:)
因此,我们现在可以为每个商店分配序列:(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!!!)
糟糕!(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:)
现在,我们只需要为选定的父级选择正确的子级即可.已经定义了用于声明关系的全局变量(ugh):(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);
});
});
}
}
而且有效!单击任务1,在其中创建了2个注释:(And it works! Clicking on Task 1, where I created 2 notes:)
单击任务2,在其中创建了1个笔记:(Clicking on Task 2, where I created 1 note:)
联络人(Contacts)
现在,让我们玩得开心,再创造一个孩子,(Now let’s have fun and create another child,) Contacts
.(.)
更新关系图:(Update the relationship map:)
var relationships : Relationship[] = [
{
parent: "Tasks",
children: ["Contacts", "Notes"]
}
];
更新HTML:(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);
就在这里:我们刚刚向Tasks添加了另一个子实体!(And there we are: we’ve just added another child entity to Tasks!)
现在,通过该练习,除了HTML用来保存(Now, having gone through that exercise, with the exception of the HTML to hold the) contacts
和(and the) contact template
本身,我们手动完成的所有其他工作都可以通过第8天的函数调用来处理.我们还必须处理删除(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!)
第8天-简化创建视图步骤(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:)
- 定义模板.(Define the template.)
- 定义容器.(Define the container.)
- 更新关系图.(Update the relationship map.)
- 创建商店视图.(Create the store view.) 步骤4现在写为:(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);
}
第9天:错误(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 };
...
错误:将CRUD操作与正确的构建器上下文相关联(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:)
- 存储应该只有一个回调.(There should only be one callback for the store.)
- 但是,构建器必须特定于CRUD操作的"上下文”.(But the builder must be specific to the “context” of the CRUD operation.)
解决方法是将CRUD操作的"上下文"传递到存储中.此刻,我只是在传递(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:)
结果是CRUD回调现在获得了生成器上下文,并将它们传递给处理程序:(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?)
否.例如,如果我有一个父子关系B-C,以及层次结构A-B-C和D-B-C,则C中记录的特定上下文与其与B记录的关系相关联.而且,当B的上下文与A的记录相关时,为商店选择的记录取决于实体路径是A-B-C还是D-B-C.请意识到” A"和" D"不同(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:)
- 正确的建造者(The correct builder)
- 所选记录(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)*而构建器和UI管理经常与(whereas the builder and UI management is working often with the)*实体类型(entity type)*层次结构.(hierarchy.)
为什么这不是错误(Why This is Not a Bug)
首先,当我们加载父子关系的记录时,它由父ID限定,这是唯一的:(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.)
该代码使用以下命令标识适当的构建器,其中包括HTML容器模板名称:(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;
}
因此,在这里,我们看到与B-C相关的构建器没有足够的信息来确定A-B-C与D-B-C的模板容器.这就是真正的错误所在.这样做的结果是,区分(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).(.)
这将在第12天,“父子模板问题"中解决.(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"]
}
];
以及相关的HTML和模板也已创建.(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
或C#中的Linq2SQL实体模型.坦白说,当我添加服务器端数据库支持时,我仍然不想做任何事情!我应该只能碰到一个地方和一个地方:描述我要查看哪些字段以及它们在哪里的模板.其他一切都应该弄清楚如何调整.(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.)
第11天:着色状态(Day 11: Colorizing Status)
这有点hack,但是我想通过给下拉菜单上色来直观地指示项目和任务的状态:(This is a bit of a hack, but I want to visually indicate the status of a project and task by colorizing the dropdown:)
这并不需要一整天,只是我有空的时候.(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;
}
是骇客.全局变量也是一个hack,就像将选定的记录索引存储在存储中一样,它应该与视图控制器关联(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),而不是商店!首先应该重新研究甚至不实施黑客!这里的整个问题是,如果可以的话,元素事件不会与保留有关"事件触发器"信息的对象耦合,因此确定与事件关联的构建器已成为黑客.这里需要的是用于绑定器,模板ID等的容器,该容器绑定到该构建器的特定UI事件-换句话说,是视图控制器.(, 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:)
- 所选记录索引与视图控制器关联.(The selected record index is associated with the view controller.)
- 视图控制器管理其子控制器列表.这样可以确保在类似A-B-C和D-B-C的方案中,B和C的控制器在根A和D方面是不同的.(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.)
第13天-审核日志(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:)
(哦,看,子任务!!!)((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;
}
hoo!(Woohoo!)
审核日志(Audit Log)
记录CRUD操作实际上是审核日志,因此我们也可以这样称呼它.这是一个由具体模型支持的具体商店:(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:)
这是为实体创建序列的示例(在本例中为”(Here’s an example of creating a sequence for an entity (in this case “) Links
“)还不存在:(") that doesn’t exist yet:)
这是商店中有关功能的代码更改的结果(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:)
第14天-服务器端持久性(Day 14 - Server-Side Persistence)
我正在.NET Core中实现服务器,因此我可以在非Windows设备上运行它,因为它实际上只是数据库操作的代理.另外,我不会使用EntityFramework或Linq2Sql.在考虑使用NoSQL数据库的同时,我希望能够灵活地在包含表的数据库上创建查询(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) join
s,这有点像PITA-并非每个NoSQL数据库引擎都实现了该功能,我真的不想处理(s, 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
我写过的MongoDB中的语法(syntax in MongoDB that I wrote about) 这里(here) .(.)
异步客户端呼叫(Async Client-Side Calls)
但是我们有一个更大的问题-AJAX调用本质上是异步的,并且我没有考虑TypeScript应用程序中的任何异步行为.如果您在阅读本文时正在考虑这一点,那么您可能会傻笑.因此,目前(我还没有决定是否要(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
带有异步AJAX调用的函数可能无法按发送的相同顺序接收.此代码需要重构,以确保(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和XML(AJAX!),在发送下一个请求之前等待服务器的响应.另一天!(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", "*");
因为TypeScript页面由一个地址(Visual Studio分配端口的localhost)提供服务,而我的服务器位于localhost:80.观察没有此标头会发生什么,这很有趣-服务器获取请求,但浏览器阻止(抛出异常)处理响应.叹.(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.)
无模型SQL(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(FluentMigrator) ,或者只是手工编码.就我个人而言,我讨厌整个方法,因为它通常意味着:(, or just hand-coded. Personally, I have come to loathe this whole approach because it usually means:)
- 数据库具有需要管理的架构.(The database has a schema that requires management.)
- 服务器端具有需要管理的模型.(The server-side has a model that requires management.)
- 客户端具有也需要管理的模型.(The client-side has a model that also requires management.) 天哪!当涉及到模式和模型时,DRY(不要重复自己)原理发生了什么?所以我要进行一个实验.您已经注意到,除了审计和序列"表"的几个具体类型外,客户端上没有任何东西的真实模型.我所谓的模型实际上隐藏在视图模板中,例如:(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" }
];
哦,您看,该视图的模板指定了该视图感兴趣的字段.在本地存储实施中,这已足够.如果我基本上有一个这样的表,那么在SQL数据库中这一切都会很好:(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)*混凝土表与混凝土柱!因此,我将做一些您要大笑的事情-根据需要动态创建表和必要的列,以使视图模板成为定义架构的"主”.是的,你没看错.仅仅因为整个世界以一种重复架构,代码隐藏模型和客户端模型的方式进行编程,并不意味着我必须这样做.当然,这会降低性能,但是我们这里不处理批量更新,而是处理异步用户驱动的更新.用户永远不会注意到,对我而言更重要的是,我将再也不必编写迁移,创建表和架构或创建镜像数据库架构的C#类.除非我在服务器端执行某些特定的业务逻辑,否则在这种情况下,可以从数据库架构中生成C#类.在F#时代,我曾遇到过一些工作,其中数据库模式可用于将Intellisense绑定到F#对象,但可悲的是,这在C#中从未发生过,并且使用动态对象具有可怕的性能,并且没有Intellisense.因此,“知道"数据库模式的编程语言支持仍然存在很大的脱节.乱跑(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.)
第15天-快速创建架构(Day 15 - Creating the Schema on the Fly)
在开始之前,需要一个小细节-与AJAX调用关联的用户ID,以便用户可以分离数据.为了进行测试,我们将使用:(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à!)
目前,我还没有输入任何内容(At this point, I haven’t entered anything for the)去做(TODO)和(and)描述(Description)字段,因此架构不知道它们存在:(fields, so the schema doesn’t know they exist:)
在我填写数据之后:(After I fill in the data:)
该模式已被修改,因为这些其他列是审核日志的一部分!(The schema has been modified because these additional columns were part of the audit log!)
我们还可以看到针对我刚刚所做的更改记录的审核日志条目:(And we can see the audit log entries logged as well for the changes I just made:)
以及所有动态创建的表格((And all the tables that were created on the fly (except for the) AuditLogStore
表):(table):)
第16天-更多错误(Day 16 - More Bugs)
实体__ID如何工作中的错误(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;
最终追加了字符1,而不增加计数.只要我没有在测试中刷新页面,一切都可以正常工作.刷新页面,新的父子关系停止工作!解决方法,缺少类型信息,无法将计数序列化为JSON中的数字而不是序列号(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
:(:)
我当然可以体会到需要为每个服务器端表和客户端用法有一个实际的模型定义,但是我真的不想走这条路!但是,实际上,在(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) 第17天-实体菜单栏(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) 在这段时间里,我和一个陌生人在一起,我当时在农场商店里,因为风在星期五造成了8个小时的停电,您真的读过这篇文章,您是否真的单击了霍桑谷农场商店链接?)我选择了对于以下行为:(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):)
点击一个实体(例如”(Click on an entity (such as “) 01 P
“),您会看到:(") and you see:)
而已!兄弟姐妹已被隐藏.单击第一个控件,在本例中为包含文本”(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:)
我将在菜单栏中单击"任务”,并假设”(I’ll clicking on Tasks in the menu bar and, assuming “) 01 P
选择”,我得到它的任务:(” is selected, I get its tasks:)
现在,我还要选择”(Now I’ll also select “)子任务(Sub-Tasks)":(":)
注意”(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:)
我发现此UI行为相当舒适:(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.) 为此,我在HTML中添加了:(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");
菜单栏和菜单项在TypeScript中定义为:(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(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:)
- 创建一个名为(Create a database called)
TaskTracker
.是的,就是这样,您不必定义任何表,它们是为您创建的.(. Yeah, that’s it, you don’t have to define any of the tables, they are created for you.) - 在服务器应用程序中,(In the server application,)Program.cs(Program.cs),设置您的连接字符串:(, set up your connection string:)
private static string connectionString = "[your connection string]";
- 以"以管理员身份"打开命令窗口,并cd到服务器应用程序的根目录,然后键入”(Open a command window “as administrator” and cd to the root of the server application, then type “)
run
“.这将构建.NET Core应用程序并启动服务器.(”. This builds .NET Core application and launches the server.) - 要退出服务器,请按(To exit the server, press)Ctrl + C(Ctrl+C)(我有一个关闭服务器的错误!)((I have a bug shutting down the server!))
- 如果需要更改IP地址或端口,请在TypeScript中进行更改(请参见上文)(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)
所以这篇文章非常重要.您可能应该一次读一次!而且这很疯狂-这是元数据驱动的,视图定义了模型,动态生成了架构,奇异的构建应用程序的方法.还有很多工作要做,例如将模板视图定义和HTML存储在特定于用户的数据库中,使用户可以灵活地自定义整个演示文稿. UI很丑陋,但实际上可以很好地完成我想要完成的工作-组织项目,任务,联系人,链接,错误和注释,这实际上对我有用!存在其他严重的疣,例如所有字段都创建为(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) 在GitHub上回购(repo on GitHub) .(.)
好吧,无论如何,这就是现在的人们!(Well, anyways, that’s all for now folks!)
许可
本文以及所有相关的源代码和文件均已获得The Code Project Open License (CPOL)的许可。
Typescript C# .NET-Core .NET Dev Design Architect 新闻 翻译