Manage attachments chunks with ASP.NET Web Api

In the previous post I spoke about a custom MultipartFormData stream provider and how it can help us to manage some custom informations included in a request message.

In that example I generated chunks form a file and I sent those to a rest service (AKA Web API) with some additional informations that were then retrieved from the custom provider.

Now I want to use these informations to manage the upload session and merge all the chunks when received.

What I need to do is define the models involved in the process and the service that manage the chunks.

Models

We have to define two stuff, the first one is the model for the chunk:


public class ChunkMetadata
{
public string Filename { get; set; }
public int ChunkNumber { get; set; }

public ChunkMetadata(string filename, int chunkNumber)
{
Filename = filename;
ChunkNumber = chunkNumber;
}
}

The ChunkNumber property deserves an explanation; is the number associated to the chunk and will be useful to understand the correct order when we’ll have to merge all of them.

The second one is the model of the session, that is the bunch of the chunks that compose the file.

First of all we define the interface:


public interface IUploadSession
{
ConcurrentBag<ChunkMetadata> Chunks { get; set; }
string Filename { get; }
long Filesize { get; }
bool AddChunk(string filename, string chunkFileName, int chunkNumber, int totalChunks);
Task MergeChunks(string path);
}

The FileName and Filesize are closely tied to the session; we need AddChunk and MergeChunks methods as well.

We also need a thread safe collection for the chunks that compose the session, so we define a CuncurrentBag collection, that is the thread safe representation of the List.

Now we can implement the model:


public class UploadSession : IUploadSession
{
public string Filename { get; private set; }
public long Filesize { get; private set; }
private int _totalChunks;
private int _chunksUploaded;

public ConcurrentBag<ChunkMetadata> Chunks { get; set; }

public UploadSession()
{
Filesize = 0;
_chunksUploaded = 0;
Chunks = new ConcurrentBag<ChunkMetadata>();
}

public bool AddChunk(string filename, string chunkFileName, int chunkNumber, int totalChunks)
{
if (Filename == null)
{
Filename = filename;
_totalChunks = totalChunks;
}

var metadata = new ChunkMetadata(chunkFileName, chunkNumber);
Chunks.Add(metadata);

_chunksUploaded = Interlocked.Increment(ref _chunksUploaded);
return _chunksUploaded == _totalChunks;
}

public async Task MergeChunks(string path)
{
var filePath = path + Filename;

using (var mainFile = new FileStream(filePath, FileMode.Create))
{
foreach (var chunk in Chunks.OrderBy(c => c.ChunkNumber))
{
using (var chunkFile = new FileStream(chunk.Filename, FileMode.Open))
{
await chunkFile.CopyToAsync(mainFile);
Filesize += chunkFile.Length;
}
}
}

foreach (var chunk in Chunks)
{
File.Delete(chunk.Filename);
}
}
}

The implementation is quite simple.

The AddChunk method add the new chunk to the collection, then increment the _chunksUploaded property with the thread safe operation Interlocked.Increment; at the end, the method returns a bool that is true if all the chunks are received, otherwise false.

The MergeChunks method deal with the retrieve of all the chunks from the file system.

It gets the collection, order by the chunk number, read the bytes from the chunks and copy those to the main file stream.

After all, the chunks are deleted.

Service

The service will have an interface like this:


public interface IUploadService
{
Guid StartNewSession();
Task<bool> UploadChunk(HttpRequestMessage request);
}

In my mind, the StartNewSession method will instantiate a new Session object and assign a new correlation id that is the unique identifier of the session.

This is the implementation:


public class UploadService : IUploadService
{
private readonly Context _db = new Context();
private readonly string _path;
private readonly ConcurrentDictionary<string, UploadSession> _uploadSessions;

public UploadService(string path)
{
_path = path;
_uploadSessions = new ConcurrentDictionary<string, UploadSession>();
}

public async Task<bool> UploadChunk(HttpRequestMessage request)
{
var provider = new CustomMultipartFormDataStreamProvider(_path);
await request.Content.ReadAsMultipartAsync(provider);
provider.ExtractValues();

UploadSession uploadSession;
_uploadSessions.TryGetValue(provider.CorrelationId, out uploadSession);

if (uploadSession == null)
throw new ObjectNotFoundException();

var completed = uploadSession.AddChunk(provider.Filename, provider.ChunkFilename, provider.ChunkNumber, provider.TotalChunks);

if (completed)
{
await uploadSession.MergeChunks(_path);

var fileBlob = new FileBlob()
{
Id = Guid.NewGuid(),
Path = _path + uploadSession.Filename,
Name = uploadSession.Filename,
Size = uploadSession.Filesize
};

_db.FileBlobs.Add(fileBlob);
await _db.SaveChangesAsync();

return true;
}

return false;
}

public Guid StartNewSession()
{
var correlationId = Guid.NewGuid();
var session = new UploadSession();
_uploadSessions.TryAdd(correlationId.ToString(), session);

return correlationId;
}
}

In the StartNewSession method we use the thread safe method TryAdd to add a new session to the CuncurrentBag.

About the UploadChunk method, we seen the first part of the implementation in the previous post.

Once the metadata is retrieved from the request, we try to find the session object with a thread safe operation.

If we don’t find the object, of course we need to throw an exception because we expect that the related session exists.

If the session exists, we add the chunk to the session and we check the result of the operation.

If is the last chunk, we merge all of them and we can do a database operation if needed.

Controller

The implementation of the controller is very simple:


public class FileBlobsController : ApiController
{
private readonly IUploadService _fileBlobsService;
private readonly Context _db = new Context();

public FileBlobsController(IUploadService uploadService)
{
_fileBlobsService = uploadService;
}

[Route("api/fileblobs/getcorrelationid")]
[HttpGet]
public IHttpActionResult GetCorrelationId()
{
return Ok(_fileBlobsService.StartNewSession());
}

[HttpPost]
public async Task<IHttpActionResult> PostFileBlob()
{
if (!Request.Content.IsMimeMultipartContent())
throw new Exception();

var result = await _fileBlobsService.UploadChunk(Request);

return Ok(result);
}
}

You can find the source code here.

Manage attachments chunks with ASP.NET Web Api

Custom MultipartFormDataStreamProvider in C#

Frequently, when we manage multipart/form requests and we send them to the server, we might want to add some additional informations.

Perhaps we might want to split a big file in chunks and we might want to add some additional informations like the id of the upload session, the chunk number, the file name and the total chunks number that compose the file.

Suppose that we use for the client side Angularjs, the code of the controller is quite simple:


.....

public AddAttachment(event) {
let attachments = event.target.files;
if (attachments.length > 0) {
let file: File = attachments[0];

this.$http.get(this.url + "/GetCorrelationId").then((correlationId) => {
let chunks = this.SplitFile(file);

for (let i = 0; i < chunks.length; i++) {
let formData = new FormData();
formData.append("file", chunks[i], file.name);
formData.append("correlationId", correlationId.data);
formData.append("chunkNumber", i + 1);
formData.append("totalChunks", chunks.length);

this.$http.post(this.url, formData, { headers: { "Content-Type": undefined } }).then((result) => {
if(result.data) {
this.Load();
}
});
}
});
}
}

private SplitFile(file: File): Array<Blob> {
let chunks = Array<Blob>();
let size = file.size;
let chunkSize = 1024 * 1024 * 10;
let start = 0;
let end = chunkSize;

while (start < size) {
let chunk = file.slice(start, end);
chunks.push(chunk);
start = end;
end += chunkSize;
}

return chunks;
}

.....

The AddAttachment method is invoked by the view; once the file is retrieved, the split method generate the array of chunks.

Then, with the $http factory we send every single chunks to the server with additional metadata.

In order to read these datas from the server side, we need to implement a custom MultipartFormData stream provider.

The first step is define the interface of our provider:


public interface ICustomMultipartFormDataStreamProvider
{
string ChunkFilename { get; }
int ChunkNumber { get; }
string CorrelationId { get; }
string Filename { get; }
int TotalChunks { get; }
void ExtractValues();
}

The interface has the same properties sent by the client, and a method that deal with extract the values from the message.

Now we can proceed with the implementation:


public class CustomMultipartFormDataStreamProvider : MultipartFormDataStreamProvider, ICustomMultipartFormDataStreamProvider
{
public string Filename { get; private set; }
public string ChunkFilename { get; private set; }
public string CorrelationId { get; private set; }
public int ChunkNumber { get; private set; }
public int TotalChunks { get; private set; }

public CustomMultipartFormDataStreamProvider(string rootPath) : base(rootPath) { }

public CustomMultipartFormDataStreamProvider(string rootPath, int bufferSize) : base(rootPath, bufferSize) { }

public override Task ExecutePostProcessingAsync()
{
foreach (var file in Contents)
{
var parameters = file.Headers.ContentDisposition.Parameters;
var filename = ExtractParameter(parameters, "filename");
if (filename != null) Filename = filename.Value.Trim('\"');
}

return base.ExecutePostProcessingAsync();
}

public void ExtractValues()
{
var chunkFileName = FileData[0].LocalFileName;
var correlationId = FormData?.GetValues("correlationId");
var chunkNumber = FormData?.GetValues("chunkNumber");
var totalChunks = FormData?.GetValues("totalChunks");

if (string.IsNullOrEmpty(chunkFileName) || correlationId == null || chunkNumber == null || totalChunks == null)
throw new Exception("Missing values in UploadChunk session.");

ChunkFilename = chunkFileName;
CorrelationId = correlationId.First();
ChunkNumber = int.Parse(chunkNumber.First());
TotalChunks = int.Parse(totalChunks.First());
}

private NameValueHeaderValue ExtractParameter(ICollection<NameValueHeaderValue> parameters, string name)
{
return parameters.FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
}

The class inherits from MultipartFormDataStreamProvider base class and implements our interface.

Two methods are implemented; the first one override ExecutePostProcessingAsync and in this method we retrieve the name of the main file.

The second one extract the custom parameters from the FormData; we retrieve also the chunk filename from the FileData object; this information is included as default information in the MultipartFormData message.

Now the informations are retrieved and we can use the custom provider in a service:


public async Task<bool> UploadChunk(HttpRequestMessage request)
{
var provider = new CustomMultipartFormDataStreamProvider(_path);
await request.Content.ReadAsMultipartAsync(provider);
provider.ExtractValues();

.....
}

The metadata will be available in the provider object.

You can find the source code here.

 

 

Custom MultipartFormDataStreamProvider in C#

Real-time search with ASP.NET and Elasticsearch

A common problem that we are faced when we have deployed our applications is improve the performance of a page or feature.

In my case for example I had a field where I could search and select a city, so the starting elements were a lot and the search was quite slow; I wanted a better user experience.

We can solve performance problems like these with the help of a cache or a full-text search.

I have chosen the last one and elastic search as full-text engine, so I’ll describe the steps that I followed to configure and use it in my application.

Installation

The first step is install the elastic search server, that you can download here.

Once installed we have to start it by executing the following executable:

<Installation path>\bin\elasticsearch.bat

This is the server log:

log

The server will take care to index the content that we will pass to it; in order to do that we need a client to use in our application; in my case the application was .NET and I used NEST.

NEST

As said above, NEST is an elastic search high level client for .NET applications.

The first step is install it in the application with nuget:

Install-package NEST

And in the package.config we’ll have:

package

Now we have all the necessary tools and we can develop the code for the search feature.

Client

We define a client class that has one responsability, that is setup the url and the default index of the client, and that instantiate it:


public class ElasticSearchClient
{
privatereadonlyIElasticClient _client;

publicElasticSearchClient(IElasticClient client)
{
_client = client;
}

publicElasticSearchClient(string uri, string indexName) : this(CreateElasticClient(uri, indexName)) {}

publicIElasticClientGetClient()
{
return_client;
}

privatestaticElasticClientCreateElasticClient(string uri, string indexName)
{
var node = newUri(uri);
var setting = newConnectionSettings(node);
setting.DefaultIndex(indexName);
returnnewElasticClient(setting);
}
}

Once instantiated, the class returns a new instance of the client; we can register it in the startup class of the application with autofac:


public partial class Startup
{
publicvoidConfiguration(IAppBuilder app)
{
var builder = newContainerBuilder();

builder.Register(c => newElasticSearchClient("http://localhost:9200", "cities"))
.AsSelf()
.SingleInstance();
...
}
}

Service base class

A service that uses an elasticsearch index should be able to do some basic operations, that concerns the logics of the full-text indexes.

We have to deal with the initialize a specific index, populate the index with the contents, obviously performs a search on the index with specific parameters.

So, we have to define an interface like this:


internal interface IElasticSearchService<T> where T : class
{
voidInit();
voidCheckIndex();
voidBulkInsert(List<T> objects);
IEnumerable<T> Search(string query);
}

I like to separate the init method, that create the index, from the checkindex method, that check if the index already exists.

Now we can implement the basic service:


public class ElasticSearchService<T> : IElasticSearchService<T> where T : class
{
protectedreadonlyContext Db = newContext();
protectedreadonlyElasticSearchClient ElasticSearchClient;
protectedreadonlystring IndexName;

publicElasticSearchService(ElasticSearchClient elasticSearchClient, string indexName)
{
ElasticSearchClient = elasticSearchClient;
IndexName = indexName;
}

publicvirtualvoidInit()
{
CheckIndex();
BulkInsert(Db.Set<T>().ToList());
}

publicvoidCheckIndex()
{
if (IndexExist()) return;
var response = CreateIndex();

if (!response.IsValid)
{
thrownewException(response.ServerError.ToString(), response.OriginalException);
}
}

publicvoidBulkInsert(List<T> objects)
{
var response = ElasticSearchClient.GetClient().IndexMany(objects, IndexName);
if (!response.IsValid)
{
thrownewException(response.ServerError.ToString(), response.OriginalException);
}
}

publicvirtualIEnumerable<T> Search(string query)
{
var results = ElasticSearchClient.GetClient().Search<T>(c => c.From(0).Size(10).Query(q => q.Prefix("_all", query)));

returnresults.Documents;
}

protectedvirtualIResponseCreateIndex()
{
var indexDescriptor = newCreateIndexDescriptor(IndexName).Mappings(ms => ms.Map<T>(m => m.AutoMap()));
returnElasticSearchClient.GetClient().CreateIndex(indexDescriptor);
}

protectedboolIndexExist()
{
returnElasticSearchClient.GetClient().IndexExists(IndexName).Exists;
}
}

The constructor accept the client and the index name.

We define a virtual init method, that check if the index exists and do a bulkinsert of a list of object; the method is virtual, we think that a derived service could override the method.

This bulkinsert method leverage the client to index the object list and the search method implements a basic search, that searchs in all the fields of the objects by using the special field _all, which contains the concatenate values of all fields.

The method returns the first 10 elements.

Createindex create a specific index with automap option, that infers the elasticsearch fields datatypes from the POCO object that we pass to it; it’s protected, so the derived class could use it.

IndexExists check if an index exists and it can be used from the derived class as well.

Service

Now we can implement a specific service, that inherits from ElasticSearchService class.

In this example I need to search in a list of cities and related districts, so I need to override the CreateIndex method like this:


public sealed class CitiesService : ElasticSearchService<City>
{
publicCitiesService(ElasticSearchClient elasticSearchClient, string indexName): base(elasticSearchClient, indexName) {}

protectedoverrideIResponseCreateIndex()
{
var indexDescriptor = newCreateIndexDescriptor(IndexName).Mappings(
ms => ms.Map<City>(m => m.AutoMap().Properties(ps =>
ps.Nested<District>(n => n
.Name(nn => nn.District)
.AutoMap()))));

returnElasticSearchClient.GetClient().CreateIndex(indexDescriptor);
}

publicoverrideIEnumerable<City> Search(string query)
{
var results = ElasticSearchClient.GetClient().Search<City>(c => c.From(0).Size(10).Query(q => q.Prefix(p => p.Name, query) || q.Term("district.name", query)));

returnresults.Documents.OrderBy(d => d.Name);
}
}

What I need to do is automap the city object and the district, that is a closely related entity of the city; so I have to map the District property as nested with the automap option as well.

Thus I will able to search for all the properties of the city and the district.

The other method that I override is the Search method; I search partially in the name of the city (Prefix) and the specific term in the district name (Term) and I returns the first 10 elements.

Now I have to register the service with autofac:


public partial class Startup
{
public void Configuration(IAppBuilder app)
{
var builder = newContainerBuilder();

builder.Register(c => newElasticSearchClient("http://localhost:9200", "cities"))
.AsSelf()
.SingleInstance();

builder.Register(c => newCitiesService(c.Resolve<ElasticSearchClient>(), "cities"))
.AsSelf()
.AsImplementedInterfaces()
.SingleInstance();

...
}
}

The last step is initialize the full text index of my service:


public partial class Startup
{
publicvoidConfiguration(IAppBuilder app)
{
var builder = newContainerBuilder();

builder.Register(c => newElasticSearchClient("http://localhost:9200", "cities"))
.AsSelf()
.SingleInstance();

builder.Register(c => newCitiesService(c.Resolve<ElasticSearchClient>(), "cities"))
.AsSelf()
.AsImplementedInterfaces()
.SingleInstance();

...

InitElasticSearchServices(containerBuilder);
}

privatestaticvoidInitElasticSearchServices(IContainer containerBuilder)
{
var citiesServices = containerBuilder.Resolve<CitiesService>();
citiesServices.Init();
}
}

I make a new instance of the service and call the Init method of the ElasticSearchService that we have seen above.

This method will create and populate the index.

Web API

Now I can use the service in my Web API, like this:


public class CitiesController : ApiController
{
privatereadonlyCitiesService _elasticSearchService;

publicCitiesController(CitiesService elasticSearchService)
{
_elasticSearchService = elasticSearchService;
}

// GET: api/Cities
publicIEnumerable<City> GetCities(string query)
{
return_elasticSearchService.Search(query);
}

protectedoverridevoidDispose(bool disposing)
{
base.Dispose(disposing);
}
}

You can find the source code of this topic here.

Real-time search with ASP.NET and Elasticsearch

Attachments management with Angular 2

A common issue that we faced in our applications is implement a component to allow the management of the attachment upload.

We need to insert a file input field in the page, grab the change event of the field, extract the file and send it to a service.

Recently I have needed to implement this functionality with Angular 2, so I’m going to explain what I have done.

Services

First of all I implement two different services, one for the file metadata and one for the blob object.

Based on a recent post, I use a base class WebApi and I define the service url:


import { Injectable } from "@angular/core";
import { Http } from "@angular/http";
import { Attachment } from "./attachment.model";
import { WebApi } from "../shared/webapi";

@Injectable()
export class AttachmentService extends WebApi<Attachment> {
constructor(public http: Http) {
super("/api/attachments", http);
}
}

The referenced service is a simple Restful service.

The second one is a service for the blob upload:


import { Injectable } from "@angular/core";
import { Http, Headers, RequestOptions, Response } from "@angular/http";
import { Observable } from "rxjs/Observable";
import { FileBlob } from "./fileBlob.model";
import { WebApi } from "../shared/webapi";

@Injectable()
export class FileBlobService extends WebApi<FileBlob> {
constructor(public http: Http) {
super("/api/fileBlobs", http);
}

public DownloadFile(id: string) {
window.open("api/fileBlobs/GetFileBlob?id=" + id, '_blank');
}

public PostFile(entity: File): Observable<File> {
let formData = new FormData();
formData.append(entity.name, entity);

return this.http.post(this.url, formData).map(this.extractData).catch(this.handleError);
}
}

The PostFile method compose a HTML FormData object with the content of the file and post it to a specific WebApi.

The DownloadFile method is simplier and call a service in a new window that returns the content of the file.

The server-side method is look like this:


public class FileBlobsController : ApiController
{
private readonly Context _db = new Context();

[ResponseType(typeof(Guid))]
public async Task<IHttpActionResult> PostFileBlob()
{
if (!Request.Content.IsMimeMultipartContent())
throw new Exception();

var provider = new MultipartMemoryStreamProvider();
await Request.Content.ReadAsMultipartAsync(provider);

HttpContent content = provider.Contents.First();
var fileName = content.Headers.ContentDisposition.FileName.Trim('\"');
var buffer = await content.ReadAsByteArrayAsync();

var fileBlob = new FileBlob()
{
Id = Guid.NewGuid(),
Name = fileName,
File = buffer
};

_db.FileBlobs.Add(fileBlob);
await _db.SaveChangesAsync();

return Ok(fileBlob.Id);
}
}

We have to use the MultiPartMemoryStreamProvider to retrieve the content of the file and store it in a specific table.

Component

We need two methods, the first one to download the existing attachment, the second one to add a new attachment:


import { Component, Input, Output, EventEmitter } from "@angular/core";
import { Constants } from "../shared/commons";
import { Attachment } from "./attachment.model";
import { FileBlobService } from "./fileBlob.service";
import { AlertService } from "../core/alert.service";

@Component({
moduleId: module.id,
selector: "attachment",
templateUrl: "attachment.component.html"
})

export class AttachmentComponent {
@Input() placeholder: string;
@Input() name: string;
@Input() validationEnabled: boolean;
@Input() attachment: Attachment;
@Output() onSaved = new EventEmitter<Attachment>();
public fileBlob: File;

constructor (private fileBlobService: FileBlobService, private alertService: AlertService) {}

public DownloadAttachment() {
this.fileBlobService.DownloadFile(this.attachment.IdFileBlob);
}

public AddAttachment(event) {
let attachments = event.target.files;
if (attachments.length > 0) {
let file:File = attachments[0];
this.fileBlobService.PostFile(file).subscribe(
(res) => {
let id: string = Constants.guidEmpty;

if (this.attachment != null)
id = this.attachment.Id;

this.attachment = {
Id: id,
IdFileBlob: res.toString(),
Name: file.name,
Size: file.size
};

this.onSaved.emit(this.attachment);
},
(error) => this.alertService.Error(error));
}
}

...
}

The AddAttachment method deserves an explanation; it accepts an event parameter, fired by the file input filed of the ui when a new attachment is selected.

The method retrieves the file from the event and pass it as a parameter to the PostFile method that we have seen above.

Once saved, an object with the file metadata is created and passed with the onSaved event to the parent component, that it deal with the object:


export class InvoiceDetailComponent {
...

public onAttachmentSaved(attachment: Attachment) {
this.attachment = attachment;
}
}

Module

We define a feature module like this:


import { NgModule } from "@angular/core";
import { HttpModule } from "@angular/http";

import { SharedModule } from "../shared/shared.module";
import { AttachmentComponent } from "./attachment.component";
import { AttachmentService } from "./attachment.service";
import { FileBlobService } from "./fileBlob.service";

let options: any = {
autoDismiss: true,
positionClass: 'toast-bottom-right',
};

@NgModule ({
imports: [
SharedModule,
HttpModule
],
exports: [
AttachmentComponent
],
declarations: [
AttachmentComponent
],
providers: [
AttachmentService,
FileBlobService
]
})

export class AttachmentModule {}

The module exports the component and provides the services discussed above.

View

We have to implement the view for the attachment module:

<div *ngIf="AttachmentIsNull()">
<label class="btn btn-primary" for="fileBlob">
<i class="fa fa-paperclip"></i> {{ "ATTACHINVOICE" | translate }}
<input id="fileBlob" type="file" [(ngModel)]="fileBlob" (change)="AddAttachment($event)" [required]="validationEnabled" style="display: none;" />
</label>
</div>
<div *ngIf="!AttachmentIsNull()">
<span *ngIf="attachment" (click)="DownloadAttachment()">{{attachment.Name}}</span>
<input type="button" class="btn btn-primary" value="Upload new" (click)="UploadNewAttachment()" />
</div>

In the view we have a file input field that bind the change event with the AddAttachment method.

The additional buttons allow us to clear the current attachment and upload a new one.

The last change is in the parent view:


<form #invoiceForm="ngForm">
<div class="form">
...
<div class="form-group">
<label for="attachment">{{ "ATTACHMENT" | translate }}</label>
<attachment placeholder="ATTACHMENT" name="attachment" [attachment]="attachment" (onSaved)="onAttachmentSaved($event)" [validationEnabled]="validationEnabled"></attachment>
</div>
</div>
</form>

We have added the attachment component in the view and we have binded the onSaved event, in order to retrieve the file metadata.

You can find the source code here.

 

 

 

 

Attachments management with Angular 2

Manage tables data with AngularJS Part 3: configuring the fields typologies

The last part of this argument is configuring the field typologies of the tables to manage.

What you need to do is specify for one or more fields of the table a couple of informations, like the typology of the field (text, number, radio, dropdown) and perhaps a list of values.

Also we might want to pass to the field external values instead of predefined values.

In order to do that and to improve the functionalities of the application, you need to implement some new features.

FieldConfiguration table

The first step is adding a new field configuration table:

[Table("FieldConfigurations")]
public class FieldConfiguration
{
public Guid Id { get; set; }
[Required]
public string Entity { get; set; }
[Required]
public string Field { get; set; }
[Required]
public string Tipology { get; set; }
public string Values { get; set; }
}

With this table we can specify for a field the tipology (text, number, radio, dropdown) and an optional list of values.

We need also update the database and implement a Web API to retrieve the datas from the new table:

public class FieldConfigurationsController : ODataController
{
private Context db = new Context();

// GET: odata/FieldConfigurations
[EnableQuery]
public IQueryable<FieldConfiguration> GetFieldConfigurations()
{
return db.FieldConfigurations;
}

// GET: odata/FieldConfigurations(5)
[EnableQuery]
public SingleResult<FieldConfiguration> GetFieldConfiguration([FromODataUri] Guid key)
{
return SingleResult.Create(db.FieldConfigurations.Where(fieldConfiguration => fieldConfiguration.Id == key));
}

// PUT: odata/FieldConfigurations(5)
public async Task<IHttpActionResult> Put([FromODataUri] Guid key, Delta<FieldConfiguration> patch)
{
Validate(patch.GetEntity());

if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

FieldConfiguration fieldConfiguration = await db.FieldConfigurations.FindAsync(key);
if (fieldConfiguration == null)
{
return NotFound();
}

patch.Put(fieldConfiguration);

try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!FieldConfigurationExists(key))
{
return NotFound();
}
else
{
throw;
}
}

return Updated(fieldConfiguration);
}

// POST: odata/FieldConfigurations
public async Task<IHttpActionResult> Post(FieldConfiguration fieldConfiguration)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

fieldConfiguration.Id = Guid.NewGuid();
db.FieldConfigurations.Add(fieldConfiguration);

try
{
await db.SaveChangesAsync();
}
catch (DbUpdateException)
{
if (FieldConfigurationExists(fieldConfiguration.Id))
{
return Conflict();
}
else
{
throw;
}
}

return Created(fieldConfiguration);
}

// PATCH: odata/FieldConfigurations(5)
[AcceptVerbs("PATCH", "MERGE")]
public async Task<IHttpActionResult> Patch([FromODataUri] Guid key, Delta<FieldConfiguration> patch)
{
Validate(patch.GetEntity());

if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

FieldConfiguration fieldConfiguration = await db.FieldConfigurations.FindAsync(key);
if (fieldConfiguration == null)
{
return NotFound();
}

patch.Patch(fieldConfiguration);

try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!FieldConfigurationExists(key))
{
return NotFound();
}
else
{
throw;
}
}

return Updated(fieldConfiguration);
}

// DELETE: odata/FieldConfigurations(5)
public async Task<IHttpActionResult> Delete([FromODataUri] Guid key)
{
FieldConfiguration fieldConfiguration = await db.FieldConfigurations.FindAsync(key);
if (fieldConfiguration == null)
{
return NotFound();
}

db.FieldConfigurations.Remove(fieldConfiguration);
await db.SaveChangesAsync();

return StatusCode(HttpStatusCode.NoContent);
}

protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}

private bool FieldConfigurationExists(Guid key)
{
return db.FieldConfigurations.Count(e => e.Id == key) > 0;
}
}

For this implementation only reading operations are needed, but still leave the other methods.

FieldConfigurations service

We need an Angular service to consume the Web API and we use the Angular $resource module.

In addition to the CRUD methods we implement additional methods like getField, getFieldConfigurationTipology and getFieldValues, that will help to retrieve specific informations about a field.

module AngularTablesDataManagerApp.Services {
import ngr = ng.resource;
import commons = AngularTablesDataManagerApp.Commons;
import models = AngularTablesDataManagerApp.Models;
import services = AngularTablesDataManagerApp.Services;

export interface IFieldConfigurationsResourceClass extends ngr.IResourceClass<ngr.IResource<models.IFieldConfiguration>> {
create(zip: models.IFieldConfiguration): ngr.IResource<models.IFieldConfiguration>;
}

export class FieldConfigurationsService {
private resource: IFieldConfigurationsResourceClass;
private $q: ng.IQService;
private $filter: ng.IFilterService;
private metadataService: services.MetadataService;
private entitySet: string = 'FieldConfigurations';
private fieldConfigurations: Array<models.IFieldConfiguration>;

constructor($resource: ngr.IResourceService, $q: ng.IQService, $filter: ng.IFilterService, metadataService: services.MetadataService) {
this.$q = $q;
this.$filter = $filter;
this.metadataService = metadataService;

this.resource = <IFieldConfigurationsResourceClass>$resource('/odata/' + this.entitySet + "(guid':key')", { key: '@Id' }, {
get: { method: 'GET' },
create: { method: 'POST', isArray: false, url: '/odata/' + this.entitySet },
save: { method: 'PUT' },
query: { method: 'GET', isArray: false, url: '/odata/' + this.entitySet },
delete: { method: 'DELETE' }
});
}

public create(fieldConfiguration: models.IFieldConfiguration) {
return this.resource.create(fieldConfiguration);
}

public save(fieldConfiguration: models.IFieldConfiguration) {
if (fieldConfiguration.Id == commons.Constants.GuidEmpty) {
return this.resource.create(fieldConfiguration);
}
else {
return this.resource.save(fieldConfiguration);
}
}

public delete(fieldConfiguration: models.IFieldConfiguration) {
return this.resource.delete({ key: fieldConfiguration.Id });
}

public getFieldConfigurationTipology(entityName: string, fieldName: string): ng.IPromise<string> {
var defer: ng.IDeferred<string> = this.$q.defer();

this.getFieldConfiguration(entityName, fieldName).then((data: models.IFieldConfiguration) => {
if (data != null) {
defer.resolve(data.Tipology);
}
else {
defer.resolve('');
}
}, (error) => {
defer.reject(error);
});

return defer.promise;
}

public getFieldValues(entityName: string, fieldName: string): ng.IPromise<Array<string>> {
var defer: ng.IDeferred<Array<string>> = this.$q.defer();
var vm = this;

this.getFieldConfiguration(entityName, fieldName).then((data: models.IFieldConfiguration) => {
if (data != null && data.Values != null && data.Values != "") {
defer.resolve(data.Values.split(";"));
}
else {
defer.resolve(new Array<string>());
}

}, (error) => {
defer.reject(error);
});

if (this.fieldConfigurations) {

}
else {

}

return defer.promise;
}

private getFieldConfiguration(entityName: string, fieldName: string): ng.IPromise<models.IFieldConfiguration> {
var defer: ng.IDeferred<models.IFieldConfiguration> = this.$q.defer();
var vm = this;

if (this.fieldConfigurations == null) {
vm.resource.query().$promise.then((data: any) => {
vm.fieldConfigurations = data["value"];
defer.resolve(vm.getField(entityName, fieldName));
}, (error) => {
defer.reject(error);
});
}
else {
defer.resolve(vm.getField(entityName, fieldName));
}

return defer.promise;
}

private getField(entityName: string, fieldName: string): models.IFieldConfiguration {
var fieldConfigurations: Array<models.IFieldConfiguration> = this.$filter('filter')(this.fieldConfigurations, { 'Entity': entityName, 'Field': fieldName }, true);
if (fieldConfigurations.length > 0) {
return fieldConfigurations[0];
}
else {
return null;
}
}

static factory() {
return (r: ngr.IResourceService, $q: ng.IQService, $filter: ng.IFilterService, MetadataService: services.MetadataService) => new FieldConfigurationsService(r, $q, $filter, MetadataService);
}
}

AngularTablesDataManager.module.factory('FieldConfigurationsService', ['$resource', '$q', '$filter', 'MetadataService', FieldConfigurationsService.factory()]);
}

The getFieldConfiguration method check if the configurations aren’t already loaded and if not invoke the Web API.

Field directive

Now you can implement the directive that will manage the fields visualization.

The directive accept some parameters, like the tipology of the field, the property object that you want to show and the possible values of the field:

module AngularTablesDataManagerApp.Directives {
import models = AngularTablesDataManagerApp.Models;
import services = AngularTablesDataManagerApp.Services;

interface IFieldDirectiveScope extends ng.IScope {
entityName: string;
contentUrl: string;
tipology: models.MetadataProperty;
property: models.RowProperty;
fieldItems: Array<models.FieldItem>;
values: Array<models.FieldItem>;
}

export class FieldDirective implements ng.IDirective {
fieldConfigurationsService: services.FieldConfigurationsService;
public restrict = 'E';
public scope = {
entityName: '=',
tipology: '=',
property: '=',
fieldItems: '='
};
public template = '<ng-include src="contentUrl" />';
public link = (scope: IFieldDirectiveScope, element: JQuery, attrs: IArguments) => {
scope.values = new Array<models.FieldItem>();

if (scope.fieldItems != null) {
for (var i = 0; i < scope.fieldItems.length; i++) {
scope.values.push(scope.fieldItems[i]);
}
}

var tipology: string = scope.tipology.Type;
this.fieldConfigurationsService.getFieldConfigurationTipology(scope.entityName, scope.tipology.Name).then((data: string) => {
if (data != "") {
tipology = data;
this.fieldConfigurationsService.getFieldValues(scope.entityName, scope.tipology.Name).then((data: Array<string>) => {
for (var i = 0; i < data.length; i++) {
var item: models.FieldItem = new models.FieldItem(data[i], data[i]);
scope.values.push(item);
}

this.openView(scope, tipology);
});
}
else {
this.openView(scope, tipology);
}
});
}

public constructor(fieldConfigurationService: services.FieldConfigurationsService) {
this.fieldConfigurationsService = fieldConfigurationService;
}

private openView(scope: IFieldDirectiveScope, tipology: string) {
if (tipology.toLowerCase().indexOf('int') != -1) {
scope.contentUrl = 'app/directives/InputNumber.html';
}
else if (tipology.toLowerCase().indexOf('radio') != -1) {
scope.contentUrl = 'app/directives/InputRadio.html'
}
else if (tipology.toLowerCase().indexOf('select') != -1) {
scope.contentUrl = 'app/directives/InputSelect.html';
}
else {
scope.contentUrl = 'app/directives/InputText.html';
}
}
}

AngularTablesDataManager.module.directive('field', ['FieldConfigurationsService', (fieldConfigurationService: services.FieldConfigurationsService) => new Directives.FieldDirective(fieldConfigurationService)]);
}

In the link function you need to call the service to retrieve the tipology of the field and to check if some values are defined.

An openView method check the field tipology and open the specific view; for example, the InputSelect view will look like this:

<select class="form-control" ng-model="property.Value">
<option ng-repeat="value in values | orderBy: 'Value'" value="{{value.Id}}" ng-selected="value.Id == property.Value">{{value.Value}}</option>
</select>

 

Grid directive

You need to extend the GridController and adding some methods to retrive the necessary informations and pass that to the Field directive:

interface IGridDirectiveScope extends ng.IScope {
entityName: string;
list: models.Grid;
item: models.Row;
rowModel: models.Row;
newItem: boolean;
fieldItems: Array<models.FieldItems>;

New(): void;
Save(item: IGridItem): void;
Delete(item: IGridItem): void;
Close(): void;
GetEntityName(): string;
GetFieldItems(fieldName: string): Array<models.FieldItem>;
IsVisiblePropertyInGrid(property: models.RowProperty): boolean;
IsVisiblePropertyInDetail(fieldName: string): boolean;
}

class GridController {
$scope: IGridDirectiveScope;
$filter: ng.IFilterService;

constructor($scope: IGridDirectiveScope, $filter: ng.IFilterService) {
this.$scope = $scope;
this.$filter = $filter;
}

public Edit(item: models.Row) {
this.$scope.item = item;
this.$scope.newItem = false;
}

public New() {
this.$scope.item = new models.Row(angular.copy(this.$scope.rowModel.Entity), this.$scope.rowModel.Name, angular.copy(this.$scope.rowModel.Properties));
this.$scope.newItem = true;
}

public Save(item: models.Row) {
var obj: IGridItem = { item: item };

this.$scope.Save(obj);
}

public Delete(item: models.Row) {
this.$scope.item = null;
var obj: IGridItem = { item: item };

this.$scope.Delete(obj);
}

public Close() {
if (!this.$scope.newItem) {
for (var i = 0; i < this.$scope.item.Properties.length; i++) {
this.$scope.item.Properties[i].Value = (<any>this.$scope.item.Entity)[this.$scope.item.Properties[i].Name];
}
}

this.$scope.item = null;
}

public GetEntityName() {
return this.$scope.entityName;
}

public GetFieldItems(fieldName: string) {
if (this.$filter('filter')(this.$scope.fieldItems, { 'FieldName': fieldName }, true).length > 0) {
return this.$filter('filter')(this.$scope.fieldItems, { 'FieldName': fieldName }, true)[0].FieldItems;
}
else {
return null;
}
}

public IsVisiblePropertyInGrid(property: models.RowProperty) {
return this.$filter('filter')(this.$scope.list.Columns, { 'Name': property.Name }, true)[0].ShowedInGrid;
}

public IsVisiblePropertyInDetail(property: models.RowProperty) {
return this.$filter('filter')(this.$scope.list.Columns, { 'Name': property.Name }, true)[0].ShowedInDetail;
}
}

A particular aspect is the fieldItems property, that is an array of objects; if you want to pass an array of values to the field directive, what we need to do is load these values in the caller of the directive and pass these as a parameter.

Now you need to change the GridItem view and use the FieldDirective:

<div class="row bootstrap-admin-no-edges-padding" style="margin-top: 10px;">
<div class="col-md-12">
<input type="button" id="btn-save" value="Save" class="btn btn-primary" ng-click="Save()" ng-disabled="form.$invalid" />
<input type="button" id="btn-delete" value="Delete" class="btn btn-danger" ng-click="Delete()" ng-show="!isNew" />
<input type="button" id="btn-close" value="Close" class="btn btn-default" ng-click="Close()" /></div>
</div>
<div class="row bootstrap-admin-no-edges-padding" style="margin-top: 10px;">
<div class="col-md-12">
<form name="form" role="form">
<div class="panel panel-default">
<div class="panel-heading">
<div class="text-muted bootstrap-admin-box-title">
{{item.Name}}</div>
</div>
<div class="panel-body">
<div class="form-group" ng-repeat="property in item.Properties | filter: IsVisibleProperty">
<label for="{{property.Name}}">{{property.Name}}:</label>
<field entity-name="GetEntityName()" tipology="GetMetadataProperty(property.Name)" property="property" field-items="GetFieldItems(property.Name)" /></div>
</div>
<div class="col-md-2"></div>
</div>
</form></div>
</div>

Cities controller

The last step is changing the main controller to retrive the new parameters that the GridDirective needed:

module AngularTablesDataManagerApp.Controllers {
import ngr = ng.resource;
import commons = AngularTablesDataManagerApp.Commons;
import models = AngularTablesDataManagerApp.Models;
import services = AngularTablesDataManagerApp.Services;

export class CitiesController {
entityName: string;
grid: models.Grid;
fieldItems: Array<models.FieldItems>;
rowModel: models.Row;
toaster: ngtoaster.IToasterService;

private citiesService: services.CitiesService;
private zipsService: services.ZipsService;
private constant: commons.Constants;

constructor(toaster: ngtoaster.IToasterService, CitiesService: services.CitiesService, ZipsService: services.ZipsService) {
this.citiesService = CitiesService;
this.zipsService = ZipsService;
this.constant = commons.Constants;
this.toaster = toaster;
this.entityName = this.citiesService.entityName;

this.grid = new models.Grid();
this.fieldItems = new Array<models.FieldItems>();
this.grid.Title = 'Cities';
this.Load();
}

private Load() {
var columns: Array<models.Column> = new Array<models.Column>();
var column: models.Column = new models.Column('Name', true, true);
columns.push(column);
column = new models.Column('IdZip', false, true);
columns.push(column);
var vm = this;

this.zipsService.getAll().then((data) => {
var zips: models.FieldItems = new models.FieldItems('IdZip');

for (var i = 0; i < data.length; i++) {
zips.FieldItems.push(new models.FieldItem(data[i].Id, data[i].Code.toString()));
}

vm.fieldItems.push(zips);

vm.citiesService.getMetadata(columns).then((data) => {
vm.grid.Columns = data;
vm.rowModel = this.citiesService.createGridData(data);

vm.citiesService.getGridData(data).then((data) => {
vm.grid.Rows = data;
vm.toaster.success('Cities loaded successfully.');
return;
}, (error) => {
vm.toaster.error('Error loading cities', error.message);
});

}, (error) => {
vm.toaster.error('Error loading cities metadata', error.data.message);
});

}, (error) => {
vm.toaster.error('Error loading zips metadata', error.data.message);
});

}

public Save(item: models.Row) {
var vm = this;
var isNew: boolean = false;

if (item.Entity.Id == commons.Constants.GuidEmpty)
isNew = true;

this.citiesService.saveGridData(item).then((data: models.Row) => {
if (isNew)
vm.grid.Rows.push(data);

this.toaster.success("City saved successfully.");
}, (error: any) => {
this.toaster.error("Error saving city", error.data.message);
});
}

public Delete(item: models.Row) {
var vm = this;
this.citiesService.deleteGridData(item).then((data: any) => {
var index = vm.grid.Rows.indexOf(item);
vm.grid.Rows.splice(index, 1);

this.toaster.success("City deleted successfully.");
}, (error: any) => {
this.toaster.error("Error deleting city", error.data.message);
});
}
}

AngularTablesDataManager.module.controller('CitiesController', CitiesController);
}

The items to pass to the GridController are an array of FieldItems; an object FieldItems has a FieldName that is the field to witch to associate the values and an array of FieldItem that are the effective values.

Using the ZipService we build an array of FieldItem associated to the IdZip field, and we’re passing that to the GridDirective.

The view is modified like this:

<grid list="vm.grid" entity-name="vm.entityName" row-model="vm.rowModel" order="Name" save="vm.Save(item)" delete="vm.Delete(item)" field-items="vm.fieldItems"></grid>

 

You can find the source code here.

 

 

Manage tables data with AngularJS Part 3: configuring the fields typologies

Manage tables data with AngularJS Part 2: nested directives

With the Angular directives you can build reusable components, witch can accept parameters and scope variables, and witch can have customized behaviours.

It can be nested and communicate between them with functions and parameters.

We can using the directives for implement a dynamic HTML table grid, with dynamics rows and columns.

Grid model

In order to display the content, a class model is needed.

This model need to have the list of columns and the row elements:

import models = AngularTablesDataManagerApp.Models;

export class Grid {
Title: string;
Columns: Array&lt;MetadataProperty&gt;;
Rows: Array&lt;Row&gt;;
}

export class Row {
Entity: models.IEntity;
Name: string;
Properties: Array&lt;RowProperty&gt;;

constructor(entity: models.IEntity, name: string, datas: Array&lt;RowProperty&gt;) {
this.Entity = entity;
this.Name = name;
this.Properties = datas;
}
}

export class MetadataProperty {
Name: string;
Type: string;
Nullable: boolean;
}

export class RowProperty {
Name: string;
Value: string;
Nullable: boolean;

constructor(name: string, value: string, nullable: boolean) {
this.Name = name;
this.Value = value;
this.Nullable = nullable;
}
}

A column is of type MetadataProperty, and have a name, a type and a nullable property.

A row is composed by the entity binded to the row, the name of the entity and an array of RowProperty; these properties contains the values of the fields, witch will be displayed in the grid.

Grid directive

The next step is the main Grid directive; the first element to define is the scope, that have the list, some other properties and the definition of the CRUD methods:

interface IGridItem {
item: models.Row;
}

interface IGridDirectiveScope extends ng.IScope {
list: models.Grid;
item: models.Row;
rowModel: models.Row;
newItem: boolean;

New(): void;
Save(item: IGridItem): void;
Delete(item: IGridItem): void;
Close(): void;
}

The implementation of the CRUD methods is in the grid directive controller:

class GridController {
$scope: IGridDirectiveScope;

constructor($scope: IGridDirectiveScope) {
this.$scope = $scope;
}

public Edit(item: models.Row) {
this.$scope.item = item;
this.$scope.newItem = false;
}

public New() {
this.$scope.item = new models.Row(angular.copy(this.$scope.rowModel.Entity), this.$scope.rowModel.Name, angular.copy(this.$scope.rowModel.Properties));
this.$scope.newItem = true;
}

public Save(item: models.Row) {
var obj: IGridItem = { item: item };

this.$scope.Save(obj);
}

public Delete(item: models.Row) {
this.$scope.item = null;
var obj: IGridItem = { item: item };

this.$scope.Delete(obj);
}

public Close() {
if (!this.$scope.newItem) {
for (var i = 0; i &lt; this.$scope.item.Properties.length; i++) {
this.$scope.item.Properties[i].Value = (&lt;any&gt;this.$scope.item.Entity)[this.$scope.item.Properties[i].Name];
}
}

this.$scope.item = null;
}
}

export class GridDirective implements ng.IDirective {
public restrict = 'E';
public templateUrl = 'app/directives/grid.html';
public scope = {
list: '=',
rowModel: '=',
order: '@order',
New: '&amp;new',
Save: '&amp;save',
Delete: '&amp;delete'
};
public controller = GridController;
}

The grid directive scope accept the list of the objects to show, the row model and the order of the grid; it also accept the references of the CRUD methods, because we’ll need to add the business logic; the & character means that we want to pass a parameter to the expression defined in the attribute.

In our case, we want to pass the object selected to the function.

Nested directives

The grid directive will hold the other directives necessary to render the grid:

interface IGridListDirectiveScope extends ng.IScope {
gridItem: models.Row;
New(): void;
}

export class GridListDirective implements ng.IDirective {
public require = '^grid';
public restrict = 'E';
public templateUrl = 'app/directives/gridList.html';
public scope = {
list: '=',
order: '='
};
public link = (scope: IGridListDirectiveScope, element: ng.IAugmentedJQuery, attrs: IArguments, gridCtrl: GridController) =&gt; {
scope.New = () =&gt; {
gridCtrl.New();
}
}
}

export class GridColumnDirective implements ng.IDirective {
public restrict = 'A';
public templateUrl = 'app/directives/gridColumn.html';
public scope = {
gridColumn: '='
};
}

interface IGridRowDirectiveScope extends ng.IScope {
gridRow: models.Row;
Edit(): void;
}

export class GridRowDirective implements ng.IDirective {
public require = '^grid';
public restrict = 'A';
public templateUrl = 'app/directives/gridRow.html';
public scope = {
gridRow: '='
};
public link = (scope: IGridRowDirectiveScope, element: ng.IAugmentedJQuery, attrs: IArguments, gridCtrl: GridController) =&gt; {
scope.Edit = () =&gt; {
gridCtrl.Edit(scope.gridRow);
}
}
}

export class GridCellDirective implements ng.IDirective {
public restrict = 'A';
public templateUrl = 'app/directives/gridCell.html';
public scope = {
gridCell: '='
};
}

interface IGridItemDirectiveScope extends ng.IScope {
item: models.Row;
metadata: Array&lt;models.MetadataProperty&gt;;
isNew: boolean;

GetMetadataProperty(Name: string): models.MetadataProperty;
Save(): void;
Delete(): void;
Close(): void;
}

export class GridItemDirective implements ng.IDirective {
$filter: ng.IFilterService;
public require = '^grid';
public restrict = 'E';
public templateUrl = 'app/directives/gridItem.html';
public scope = {
item: '=',
metadata: '=',
isNew: '='
};
public link = (scope: IGridItemDirectiveScope, element: ng.IAugmentedJQuery, attrs: IArguments, gridCtrl: GridController) =&gt; {
scope.Save = () =&gt; {
gridCtrl.Save(scope.item);
}

scope.Delete = () =&gt; {
gridCtrl.Delete(scope.item);
}

scope.Close = () =&gt; {
gridCtrl.Close();
}

scope.GetMetadataProperty = (Name: string) =&gt; {
return this.$filter('filter')(scope.metadata, { 'Name': Name })[0];
}
}

public constructor($filter: ng.IFilterService) {
this.$filter = $filter;
}
}

The require attribute with ^ character means that the directive searches for the controller on its own element or its parent.

So, in the link function of the directive you can call a function in the controller of the directive specified in the require attribute.

Grid directives views

The implementation of the views is simple; the first view is the grid:

&lt;div ng-show=&quot;item == null&quot;&gt;
&lt;grid-list list=&quot;list&quot; order=&quot;order&quot;&gt;&lt;/grid-list&gt;
&lt;/div&gt;
&lt;div ng-show=&quot;item != null&quot;&gt;
&lt;grid-item item=&quot;item&quot; metadata=&quot;list.Columns&quot; is-new=&quot;newItem&quot;&gt;&lt;/grid-item&gt;
&lt;/div&gt;

The GridList view contains the columns and the rows of the grid:

&lt;div class=&quot;row bootstrap-admin-no-edges-padding&quot; style=&quot;margin-top: 10px;&quot;&gt;
&lt;div class=&quot;col-md-12&quot;&gt;
&lt;input type=&quot;button&quot; id=&quot;btn-new&quot; value=&quot;New&quot; class=&quot;btn btn-primary&quot; ng-click=&quot;New()&quot; /&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;row bootstrap-admin-no-edges-padding&quot; style=&quot;margin-top: 10px;&quot;&gt;
&lt;div class=&quot;col-md-12&quot;&gt;
&lt;div class=&quot;panel panel-default&quot;&gt;
&lt;div class=&quot;panel-heading&quot;&gt;
&lt;div class=&quot;text-muted bootstrap-admin-box-title&quot;&gt;
{{list.Title}}
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;panel-body&quot;&gt;
&lt;table class=&quot;table table-hover&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th ng-repeat=&quot;column in list.Columns&quot; grid-column=&quot;column&quot;&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr ng-repeat=&quot;row in list.Rows | orderBy: 'Entity.' + order&quot; grid-row=&quot;row&quot;&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;

GridColumn, GridRow and GridCell compose the table structure; the GridItem view is the detail of the row:

&lt;div class=&quot;row bootstrap-admin-no-edges-padding&quot; style=&quot;margin-top: 10px;&quot;&gt;
&lt;div class=&quot;col-md-12&quot;&gt;
&lt;input type=&quot;button&quot; id=&quot;btn-save&quot; value=&quot;Save&quot; class=&quot;btn btn-primary&quot; ng-click=&quot;Save()&quot; ng-disabled=&quot;form.$invalid&quot; /&gt;
&lt;input type=&quot;button&quot; id=&quot;btn-delete&quot; value=&quot;Delete&quot; class=&quot;btn btn-danger&quot; ng-click=&quot;Delete()&quot; ng-show=&quot;!isNew&quot; /&gt;
&lt;input type=&quot;button&quot; id=&quot;btn-close&quot; value=&quot;Close&quot; class=&quot;btn btn-default&quot; ng-click=&quot;Close()&quot; /&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;row bootstrap-admin-no-edges-padding&quot; style=&quot;margin-top: 10px;&quot;&gt;
&lt;div class=&quot;col-md-12&quot;&gt;
&lt;form name=&quot;form&quot; role=&quot;form&quot;&gt;
&lt;div class=&quot;panel panel-default&quot;&gt;
&lt;div class=&quot;panel-heading&quot;&gt;
&lt;div class=&quot;text-muted bootstrap-admin-box-title&quot;&gt;
{{item.Name}}
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;panel-body&quot;&gt;
&lt;div class=&quot;form-group&quot; ng-repeat=&quot;property in item.Properties&quot;&gt;
&lt;label for=&quot;{{property.Name}}&quot;&gt;{{property.Name}}:&lt;/label&gt;
&lt;input field tipology=&quot;GetMetadataProperty(property.Name)&quot; id=&quot;{{property.Name}}&quot; class=&quot;form-control&quot; ng-model=&quot;property.Value&quot; ng-required=&quot;!property.Nullable&quot; /&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;col-md-2&quot;&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/form&gt;
&lt;/div&gt;
&lt;/div&gt;

Application

To use the directive you need to load the model, the rows and define the CRUD methods:

import ngr = ng.resource;
import commons = AngularTablesDataManagerApp.Commons;
import models = AngularTablesDataManagerApp.Models;
import services = AngularTablesDataManagerApp.Services;

export class CitiesController {
grid: models.Grid;
rowModel: models.Row;
toaster: ngtoaster.IToasterService;

private citiesService: services.CitiesService;
private constant: commons.Constants;

constructor(toaster: ngtoaster.IToasterService, CitiesService: services.CitiesService) {
this.citiesService = CitiesService;
this.constant = commons.Constants;
this.toaster = toaster;

this.grid = new models.Grid();
this.grid.Title = 'Cities';
this.Load();
}

private Load() {
var columns: Array&lt;string&gt; = new Array&lt;string&gt;('Name');
var vm = this;

this.citiesService.getMetadata(columns).then((data) =&gt; {
vm.grid.Columns = data;
vm.rowModel = this.citiesService.createGridData(data);

this.citiesService.getGridData(data).then((data) =&gt; {
vm.grid.Rows = data;
vm.toaster.success('Cities loaded successfully.');
return;
}, (error) =&gt; {
vm.toaster.error('Error loading cities', error.message);
});

}, (error) =&gt; {
vm.toaster.error('Error loading cities metadata', error.data.message);
});
}

public Save(item: models.Row) {
var vm = this;
var isNew: boolean = false;

if (item.Entity.Id == commons.Constants.GuidEmpty)
isNew = true;

this.citiesService.saveGridData(item).then((data: models.Row) =&gt; {
if (isNew)
vm.grid.Rows.push(data);

this.toaster.success(&quot;City saved successfully.&quot;);
}, (error: any) =&gt; {
this.toaster.error(&quot;Error saving city&quot;, error.data.message);
});
}

public Delete(item: models.Row) {
var vm = this;
this.citiesService.deleteGridData(item).then((data: any) =&gt; {
var index = vm.grid.Rows.indexOf(item);
vm.grid.Rows.splice(index, 1);

this.toaster.success(&quot;City deleted successfully.&quot;);
}, (error: any) =&gt; {
this.toaster.error(&quot;Error deleting city&quot;, error.data.message);
});
}
}

The final step is the view:

&lt;grid list=&quot;vm.grid&quot; row-model=&quot;vm.rowModel&quot; order=&quot;Name&quot; save=&quot;vm.Save(item)&quot; delete=&quot;vm.Delete(item)&quot;&gt;&lt;/grid&gt;

 

You can find the source code here.

 

 

Manage tables data with AngularJS Part 2: nested directives

Manage tables data with AngularJS Part 1: tables metadata

During the deployment of an AngularJS app, we often develop controllers and views to manage data of basic tables,  such as zip, city, country and so on.

We need to offer to the users the CRUD operations of these tables; the functionalities and the structure of these controllers/view are very similar and you need to implement a lot of similar code to do that.

What you can do is implement the AngularJS directives that are able to show the content of basic tables and manage the CRUD operations.

The start point is a simple AngularJS SPA that consume ASP.NET Web API; the application using Entity Framework 6 code first and the only entity is city.

OData controller

Once the table is configured, what we need to do is implement an OData controller that will expose the CRUD operations of the Cities table and the metadata.

For this example we implement and endpoint that support OData Version 3:

public class CitiesController : ODataController
{
private Context db = new Context();

// GET: odata/Cities
[EnableQuery]
public IQueryable<City> GetCities()
{
return db.Cities;
}

// GET: odata/Cities(5)
[EnableQuery]
public SingleResult<City> GetCity([FromODataUri] Guid key)
{
return SingleResult.Create(db.Cities.Where(city => city.Id == key));
}

// PUT: odata/Cities(5)
public async Task<IHttpActionResult> Put([FromODataUri] Guid key, Delta<City> patch)
{
Validate(patch.GetEntity());

if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

City city = await db.Cities.FindAsync(key);
if (city == null)
{
return NotFound();
}

patch.Put(city);

try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!CityExists(key))
{
return NotFound();
}
else
{
throw;
}
}

return Updated(city);
}

// POST: odata/Cities
public async Task<IHttpActionResult> Post(City city)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

db.Cities.Add(city);

try
{
await db.SaveChangesAsync();
}
catch (DbUpdateException)
{
if (CityExists(city.Id))
{
return Conflict();
}
else
{
throw;
}
}

return Created(city);
}

// PATCH: odata/Cities(5)
[AcceptVerbs("PATCH", "MERGE")]
public async Task<IHttpActionResult> Patch([FromODataUri] Guid key, Delta<City> patch)
{
Validate(patch.GetEntity());

if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

City city = await db.Cities.FindAsync(key);
if (city == null)
{
return NotFound();
}

patch.Patch(city);

try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!CityExists(key))
{
return NotFound();
}
else
{
throw;
}
}

return Updated(city);
}

// DELETE: odata/Cities(5)
public async Task<IHttpActionResult> Delete([FromODataUri] Guid key)
{
City city = await db.Cities.FindAsync(key);
if (city == null)
{
return NotFound();
}

db.Cities.Remove(city);
await db.SaveChangesAsync();

return StatusCode(HttpStatusCode.NoContent);
}

protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}

private bool CityExists(Guid key)
{
return db.Cities.Count(e => e.Id == key) > 0;
}
}

Then, you need to configure the OData Endpoint in the WebApiConfig.cs:

public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<City>("Cities");
config.Routes.MapODataServiceRoute("odata", "odata", builder.GetEdmModel());
}
}

OData controller is helpful to retrieve the metadata and you can using $metadata keyword:

http://host_name/odata/$metadata

We’ll receive a response like this:

<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="1.0" xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx">
<edmx:DataServices m:DataServiceVersion="3.0" m:MaxDataServiceVersion="3.0" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
<Schema Namespace="AngularTablesDataManager.DataLayer" xmlns="http://schemas.microsoft.com/ado/2009/11/edm">
<EntityType Name="City">
<Key>
<PropertyRef Name="Id" />
</Key>
<Property Name="Id" Type="Edm.Guid" Nullable="false" />
<Property Name="Name" Type="Edm.String" />
</EntityType>
<EntityType Name="Zip">
<Key>
<PropertyRef Name="Id" />
</Key>
<Property Name="Id" Type="Edm.Guid" Nullable="false" />
<Property Name="Code" Type="Edm.Int16" Nullable="false" />
</EntityType>
</Schema>
<Schema Namespace="Default" xmlns="http://schemas.microsoft.com/ado/2009/11/edm">
<EntityContainer Name="Container" m:IsDefaultEntityContainer="true">
<EntitySet Name="Cities" EntityType="AngularTablesDataManager.DataLayer.City" />
<EntitySet Name="Zips" EntityType="AngularTablesDataManager.DataLayer.Zip" />
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>

AngularJS service

The next step is the implementation of the AngularJS service that parse the metadata; first of all, a class that define the structure of metadata is required:

export class MetadataProperty {
Name: string;
Type: string;
Nullable: boolean;
}

Then, the MetadataService service:

export class MetadataService {
private $http: ng.IHttpService;
private $q: ng.IQService;

constructor($http: ng.IHttpService, $q: ng.IQService) {
this.$http = $http;
this.$q = $q;
}

public getMetadata(entityName: string): ng.IPromise<Array<models.MetadataProperty>> {
var defer: ng.IDeferred<any> = this.$q.defer();

var req = {
method: 'GET',
url: '/odata/$metadata'
};

this.$http(req).then(function (result) {
var data: string = result.data.toString();
var xmlDoc: XMLDocument = $.parseXML(data);
var xml: JQuery = $(xmlDoc);
var properties: Array<models.MetadataProperty> = new Array<models.MetadataProperty>();

xml.find('EntityType').find('Property').each(function () {
var metadataProperty: models.MetadataProperty = new models.MetadataProperty();
metadataProperty.Name = $(this).attr('Name');
metadataProperty.Type = $(this).attr('Type');
metadataProperty.Nullable = ($(this).attr('Nullable') != null) && ($(this).attr('Nullable').toLowerCase() == 'true');

properties.push(metadataProperty);
});

return defer.resolve(properties);
});

return defer.promise;
}

static factory() {
return ($http: ng.IHttpService, $q: ng.IQService) => new MetadataService($http, $q);
}
}

To consume the OData Controller you can use the $resource factory, that allows to perform CRUD operations easily, and extend the service with an additional method called getMetadata:

const entityName: string = 'Cities';
import ngr = ng.resource;
import commons = AngularTablesDataManagerApp.Commons;
import models = AngularTablesDataManagerApp.Models;
import services = AngularTablesDataManagerApp.Services;

export interface ICitiesResourceClass extends ngr.IResourceClass<ngr.IResource<models.ICity>> {
create(order: models.ICity): ngr.IResource<models.ICity>;
}

export class CitiesService implements models.IService {
private resource: ICitiesResourceClass;
private $q: ng.IQService;
private metadataService: services.MetadataService;

constructor($resource: ngr.IResourceService, $q: ng.IQService, MetadataService: services.MetadataService) {
this.$q = $q;
this.metadataService = MetadataService;

this.resource = <ICitiesResourceClass>$resource('/odata/' + entityName + '/:id', { id: '@Id' }, {
get: { method: "GET" },
create: { method: "POST" },
save: { method: "PUT" },
query: { method: "GET", isArray: false },
delete: { method: "DELETE" }
});
}

public create(order: models.ICity) {
return this.resource.create(order);
}

public save(order: models.ICity) {
if (order.Id == commons.Constants.GuidEmpty) {
return this.resource.create(order);
}
else {
return this.resource.save(order);
}
}

public delete(order: models.ICity) {
return this.resource.remove(order);
}

public getAll() {
var datas: ngr.IResourceArray<ngr.IResource<models.ICity>>
var defer: ng.IDeferred<any> = this.$q.defer();

this.resource.query().$promise.then((data: any) => {
datas = data["value"];

return defer.resolve(datas);
}, (error) => {
return defer.reject(datas);
});

return defer.promise;
}

public getMetadata(): ng.IPromise<Array<models.MetadataProperty>> {
return this.metadataService.getMetadata(entityName);
}

static factory() {
return (r: ngr.IResourceService, $q: ng.IQService, MetadataService: services.MetadataService) => new CitiesService(r, $q, MetadataService);
}
}

This method can be called from a controller; we’ll obtain an array of MetadataProperty objects that represents the structure of the Cities table.

import ngr = ng.resource;
import commons = AngularTablesDataManagerApp.Commons;
import models = AngularTablesDataManagerApp.Models;
import services = AngularTablesDataManagerApp.Services;

export class CitiesController {
cities: ngr.IResourceArray<ngr.IResource<models.ICity>>;
city: models.ICity;
title: string;
toaster: ngtoaster.IToasterService;

private citiesService: services.CitiesService;
private constant: commons.Constants;
private metadataProperties: Array<models.MetadataProperty>;

constructor(toaster: ngtoaster.IToasterService, CitiesService: services.CitiesService) {
this.citiesService = CitiesService;
this.constant = commons.Constants;

this.toaster = toaster;
this.title = 'Cities';

this.Load();
}

private Load() {
this.citiesService.getMetadata().then((data) => {
this.metadataProperties = data;
}, (error) => {
this.toaster.error('Error loading cities medatad', error.data.message);
});

this.citiesService.getAll().then((data) => {
this.cities = data;
this.toaster.success('Cities loaded successfully.');
return;
}, (error) => {
this.toaster.error('Error loading cities', error.message);
});
}
}

AngularTablesDataManager.module.controller('CitiesController', CitiesController);

 

You can find the source code here.

 

Manage tables data with AngularJS Part 1: tables metadata