Tutorial MVC NET
Tutorial MVC NET
Prerequisites
This tutorial assumes that you have Visual Studio 2010 installed, along with ASP.NET MVC 2/4. You may use any edition
of Visual Studio or you may use Visual Web Developer. Visual Studio Web Developer 2010 Express will also work for this
tutorial, but the screenshots included will be from Visual Studio 2012 Professional.
You will also need to have an installation of Couchbase Server 2.0 and have obtained the latest Couchbase .NET Client
Library, version 1.3 or higher.
You also may use an older version of ASP.NET MVC if you do not have MVC 4 installed, but as with using Visual Web
Developer or Visual Studio 2012, the templates shown in the screenshots will vary from what you see.
You should also have installed the beer-sample database on your Couchbase Server.
hPot-Tech
[Start with an Empty application using the Razor view engine for the MVC template.] optional
hPot-Tech
Next youll need to add a reference to the Couchbase .NET Client Library.Add references to Couchbase.dll,
Enyim.Caching.dll and the dependencies Newtonsoft.Json.dll and Hammock.dll. These assemblies are found in the zip
file and should be referenced in your project.
hPot-Tech
As a JSON document database, Couchbase supports a natural mapping of domain objects to data items. In other words,
theres very little difference between the representation of your data as a class in C# and the representation of your data
as a document in Couchbase. Your object becomes the schema defined in the JSON document.
When working with domain objects that will map to documents in Couchbase, its useful, but not required, to define a base
class from which your model classes will derive. This base class will be abstract and contain two properties, Id and
Type.
hPot-Tech
Right click on the Models directory and add a new class named ModelBase and include the following code.
public abstract class ModelBase
{
public virtual string Id { get; set; }
public abstract string Type { get; }
}
Note that the Type method is abstract and readonly. It will be implemented by subclasses simply by returning a hardcoded string, typically matching the class name, lower-cased. The purpose of the Type property is to provide taxonomy to
the JSON documents stored in your Couchbase bucket.
Next, create a new class namedin the Models directory of your project. This class will be a plain old CLR object (POCO)
that simply has properties mapping to the properties of brewery documents in the beer-sample bucket. It will also
extend ModelBase .
public class Brewery : ModelBase
{
public string Name { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Code { get; set; }
public string Country { get; set; }
hPot-Tech
hPot-Tech
Sample Document
{
"name": "Thomas Hooker Brewing",
"city": "Bloomfield",
"state": "Connecticut",
"code": "6002",
"country": "United States",
"phone": "860-242-3111",
"website": "http://www.hookerbeer.com/",
"type": "brewery",
"updated": "2010-07-22 20:00:20",
"description": "Tastings every Saturday from 12-6pm, and 1st and 3rd Friday of every month from 5-8.",
"address": [
"16 Tobey Road"
],
"geo": {
"accuracy": "RANGE_INTERPOLATED",
"lat": 41.8087,
hPot-Tech
"lng": -72.7108
}
}
The process of creating an instance of a CouchbaseClient is expensive. There is a fair amount of overhead as the client
establishes connections to the cluster. It is therefore recommended to minimize the number of times that a client instance
is created in your application. The simplest approach is to create a static property or singleton that may be accessed from
data access code. Using the RepositoryBase , setting up a protected static property will provide access for subclasses.
public abstract class RepositoryBase<T> where T : ModelBase
{
protected static CouchbaseClient _Client { get; set; }
static RepositoryBase()
hPot-Tech
{
_Client = new CouchbaseClient();
}
}
hPot-Tech
10
hPot-Tech
11
hPot-Tech
12
If you click the link next to the Filter Results button, you will see the JSON that is returned to the CouchbaseClient when
querying a view. Notice the id property found in each row. That is the key that was used to store the document.
{"total_rows":1412,"rows":[
{"id":"21st_amendment_brewery_cafe","key":null,"value":null},
{"id":"357","key":null,"value":null},
{"id":"3_fonteinen_brouwerij_ambachtelijke_geuzestekerij","key":null,"value":null},
{"id":"512_brewing_company","key":null,"value":null},
{"id":"aass_brewery","key":null,"value":null},
{"id":"abbaye_de_leffe","key":null,"value":null},
{"id":"abbaye_de_maredsous","key":null,"value":null},
{"id":"abbaye_notre_dame_du_st_remy","key":null,"value":null},
{"id":"abbey_wright_brewing_valley_inn","key":null,"value":null},
{"id":"aberdeen_brewing","key":null,"value":null}
]
}
hPot-Tech
13
With the view created, the next step is to modify the RepositoryBase to have a GetAll method.
The initial implementation of GetAll will simply return all breweries using the generic GetView<T> method
of CouchbaseClient . The third parameter instructs CouchbaseClient to retrieve the original document rather than
deserialize the value of the view row.
public virtual IEnumerable<T> GetAll(int limit = 0)
{
var view = _Client.GetView<T>("dev_breweries", "all", true).Stale(StaleMode.False);
if (limit > 0) view.Limit(limit);
return view;
}
RepositoryBase is a generic and abstract class, so obviously it cannot be used directly. Create a new class in Models
named BreweryRepository. The code for this class is very minimal, as it will rely on its base class for most functionality.
public class BreweryRepository : RepositoryBase<Brewery>
{
}
hPot-Tech
14
hPot-Tech
15
Or
The Index method of the BreweriesController will be used to display the list of breweries. To allow the new controller to
access brewery data, it will need an instance of a BreweryRepository . Create a public property of
type BreweryRepository and instantiate it in the default constructor.
public BreweryRepository BreweryRepository { get; set; }
public BreweriesController()
{
BreweryRepository = new BreweryRepository();
}
Then inside of the Index method, add a call to BreweryRepository s GetAll method and pass its results to the view as
its model.
hPot-Tech
16
The last step to displaying the list of breweries is to create the [Razor] view (as in MVC views, not Couchbase views). In
the Views directory, create a new directory named Breweries. Right click on that new directory and select Add ->
View. Name the view Index and create it as a strongly typed (to the Brewery class) view with List scaffolding. This
template will create a Razor view that loops over the brewery results, displaying each as a row in an HTML table.
hPot-Tech
17
hPot-Tech
18
At this point, you should build your application and navigate to the Breweries path [ctrl + F5]
If all went well, you should see a list of breweries.
http://localhost:3854/Breweries
hPot-Tech
19
Brewery CRUD
The MVC scaffolding that created the Razor template to list breweries also included links to create, show, edit and delete
breweries. Using more scaffolding, these CRUD features are easily implemented.
Add the following BuildKey method to the RepositoryBase to allow for default key creation based on the Id property.
protected virtual string BuildKey(T model)
{
if (string.IsNullOrEmpty(model.Id))
{
return Guid.NewGuid().ToString();
}
return
return model.Id.ToLower();
BuildKey will default to a GUID string when no Id is provided. Its also virtual so that subclasses are able to override the
default behavior.
hPot-Tech
20
When storing a Brewery instance in Couchbase Server, it first has to be serialized into a JSON string. An important
consideration is how to map the properties of the Brewery to properties of the JSON document.
JSON.NET (from Newtonsoft.Json) will by default serialize all properties. However, ModelBase objects all have an Id
property that shouldnt be serialized into the stored JSON. That Id is already being used as the documents key (in the
key/value operations), so it would be redundant to store it in the JSON.
JSON.NET supports various serialization settings, including which properties should be included in serialization.
In RepositoryBase , create a serializAndIgnoreId method and a private DocumentIdContractResolver class as shown
below.
private string serializeAndIgnoreId(T obj)
{
var json = JsonConvert.SerializeObject(obj,
new JsonSerializerSettings()
{
ContractResolver = new DocumentIdContractResolver(),
});
return json;
}
hPot-Tech
21
The DocumentIdContractResolver will prevent the Id property from being saved into the JSON. It also
extends CamelCasePropertyNamesContractResolver to provide camel-cased properties in the JSON output.
With this new plumbing in place, its now possible to complete the Create , Update and Save methods. (RepositoryBase)
hPot-Tech
22
Get will return the object or null if not found, while throwing a swallowed exception.
public virtual T Get(string key)
{
var result = _Client.ExecuteGet<string>(key);
if (result.Exception != null) throw result.Exception;
if (result.Value == null)
{
return null;
}
hPot-Tech
23
Brewery Forms
With the new methods implemented, its time to create the scaffolding for the CRUD forms. The first task will be to create
an edit form. Open the BreweriesController and locate the Edit methods that were generated by the Add Controller
wizard.
In the HTTP GET override of Edit , modify it as shown below. This action will retrieve the Brewery and pass it to the view
as the model. Note the change from an int id parameter to a string id parameter.
public ActionResult Edit(string id)
{
var brewery = BreweryRepository.Get(id);
return View(brewery);
}
hPot-Tech
24
Update the Edit method that handles POSTs as shown below. Validation and error handling are intentionally being
omitted for brevity.
[HttpPost]
public ActionResult Edit(string id, Brewery brewery)
{
try
{
// TODO: Add update logic here
BreweryRepository.Update(brewery);
return RedirectToAction("Index");
}
catch
{
return View();
}}
The edit form will be created using scaffolding, as was the case with the listing page. Right click on the Breweries folder
in the Views directory and click Add -> View. Name the view Edit and strongly type it to a Brewery with Edit scaffolding.
hPot-Tech
25
Rebuild the application and return to the brewery listing page. Click on an Edit link and you should see the edit form
loaded with the details for that brewery. Edit some values on the form and click save. You should see your changes
persisted on the listing page.
hPot-Tech
26
hPot-Tech
27
The Details action looks very much like Edit . Get the Brewery and provide it as the model for the view.
public ActionResult Details(string id)
{
var brewery = BreweryRepository.Get(id);
return View(brewery);
}
Create a scaffolding form for Details using the same process as was used with Edit .
hPot-Tech
28
Rebuild and return to the list page. Click on a Details link. You should see a page listing the data for that brewery.
hPot-Tech
29
hPot-Tech
30
The Create and Edit actions of the BreweriesController are quite similar, save for the fact that Creates GET method
doesnt provide a model to the view. Again, error handling and validation are being omitted for brevitys sake.
public ActionResult Create()
{
return View();
}
[HttpPost]
public ActionResult Create(Brewery brewery)
{
try
{
// TODO: Add insert logic here
BreweryRepository.Create(brewery);
return RedirectToAction("Index");
}
catch
{
return View();
}
}
Go through the scaffolding process again to add a create view for the Create action. Rebuild and click the Create New
link on the list page to test the new form. Breweries (for now) are sorted by key and limited to 20, so you might not see
yours in the list. If you want to verify your create action worked, use brewery name that starts with a numeric value (e.g.,
123 Brewery).
Also now (because of the stale setting), if you create a new Brewery, it should appear after a redirect and should not
require a refresh.
hPot-Tech
31
The last piece required to complete the CRUD functionality for breweries is to implement the delete
form. Update the Delete actions in BreweriesController as shown below.
public ActionResult Delete(string id)
{
var brewery = BreweryRepository.Get(id);
return View(brewery);
}
[HttpPost]
public ActionResult Delete(string id, Brewery brewery)
{
try
{
// TODO: Add delete logic here
BreweryRepository.Delete(id);
return RedirectToAction("Index");
}
catch
{
return View();
}}
To create the delete form, simply go through the Add View process again and choose scaffolding for delete
(dont forget to choose the Brewery model).
hPot-Tech
32
Click on Edit
hPot-Tech
33
Update City:
Save
hPot-Tech
34
Click on details:
hPot-Tech
35
hPot-Tech
36
hPot-Tech
37
hPot-Tech
38
Congrats!
hPot-Tech
39
hPot-Tech
40
Controller:
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
System.Web.Mvc;
CouchbaseBeersWeb.Models;
namespace CouchbaseBeersWeb.Controllers
{
public class BreweriesController : Controller
{
public BreweryRepository BreweryRepository { get; set; }
public BreweriesController()
{
BreweryRepository = new BreweryRepository();
}
//
// GET: /Breweries/
public ActionResult Index()
{
var breweries = BreweryRepository.GetAll(50);
return View(breweries);
}
//
// GET: /Breweries/Details/5
public ActionResult Details(string id)
{
hPot-Tech
41
hPot-Tech
42
hPot-Tech
43
//
// POST: /Breweries/Delete/5
[HttpPost]
public ActionResult Delete(string id, Brewery brewery)
{
try
{
// TODO: Add delete logic here
BreweryRepository.Delete(id);
return RedirectToAction("Index");
}
catch
{
return View();
}
}
}
}
hPot-Tech
44
Models:
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
namespace CouchbaseBeersWeb.Models
{
public class Brewery : ModelBase
{
public string Name { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Code { get; set; }
public string Country { get; set; }
public string Phone { get; set; }
public string Website { get; set; }
public DateTime Updated { get; set; }
public string Description { get; set; }
public IList<string> Addresses { get; set; }
public IDictionary<string, object> Geo { get; set; }
public override string Type
{
get { return "brewery"; }
}
}
}
hPot-Tech
45
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
Couchbase;
Couchbase.Configuration;
using
using
using
using
using
using
Enyim.Caching.Memcached.Results;
Enyim.Caching.Memcached;
Newtonsoft.Json;
Newtonsoft.Json.Serialization;
System.Reflection;
Couchbase.Operations;
namespace CouchbaseBeersWeb.Models
{
public class BreweryRepository : RepositoryBase<Brewery>
{
/// <summary>
/// Create a Brewery, blocking until persisted to master node
/// Views will not consider a new record for an index
/// until persisted to disk.
/// </summary>
/// <param name="value">Brewery to create</param>
/// <returns>Status code (0 on success)</returns>
public int Create(Brewery brewery)
{
return base.Create(brewery, PersistTo.One);
}
/// <summary>
hPot-Tech
46
hPot-Tech
47
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
namespace CouchbaseBeersWeb.Models
{
public abstract class ModelBase
{
public virtual string Id { get; set; }
public abstract string Type { get; }
}
}
hPot-Tech
48
using
using
using
using
using
using
using
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Web;
Couchbase;
Couchbase.Configuration;
Enyim.Caching.Memcached;
Newtonsoft.Json;
Enyim.Caching.Memcached.Results;
Couchbase.Extensions;
Couchbase.Operations;
Newtonsoft.Json.Serialization;
System.Reflection;
namespace CouchbaseBeersWeb.Models
{
public abstract class RepositoryBase<T> where T : ModelBase
{
protected static CouchbaseClient _Client { get; set; }
static RepositoryBase()
{
_Client = new CouchbaseClient();
}
/// <summary>
/// Retrieve a document from a bucket
/// </summary>
/// <param name="key">Key of document</param>
/// <returns>When key is found, returns document, else null.</returns>
public virtual T Get(string key)
{
var result = _Client.ExecuteGet<string>(key);
if (result.Exception != null) throw result.Exception;
hPot-Tech
49
if (result.Value == null)
{
return null;
}
var model = JsonConvert.DeserializeObject<T>(result.Value);
model.Id = key; //Id is not serialized into the JSON document on store, so need to set
it before returning
return model;
}
public virtual IEnumerable<T> GetAll(int limit = 0)
{
var view = _Client.GetView<T>("dev_breweries", "all", true).Stale(StaleMode.False);
if (limit > 0) view.Limit(limit);
return view;
}
public virtual int Create(T value, PersistTo persistTo = PersistTo.Zero)
{
var result = _Client.ExecuteStore(StoreMode.Add, BuildKey(value),
serializeAndIgnoreId(value), persistTo);
if (result.Exception != null) throw result.Exception;
return result.StatusCode.Value;
}
50
<summary>
Default key generation strategy. If no key is provided
then GUID is created. Subclasses should override
when Id property shouldn't be used.
</summary>
<param name="model"></param>
<returns></returns>
hPot-Tech
51
hPot-Tech
52
Views:
Create.aspx:
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<CouchbaseBeersWeb.Models.Brewery>" %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
Create
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>Create</h2>
<% using (Html.BeginForm()) {%>
<%: Html.ValidationSummary(true) %>
<fieldset>
<legend>Fields</legend>
<div class="editor-label">
<%: Html.LabelFor(model => model.Name) %>
</div>
<div class="editor-field">
<%: Html.TextBoxFor(model => model.Name) %>
<%: Html.ValidationMessageFor(model => model.Name) %>
</div>
<div class="editor-label">
<%: Html.LabelFor(model => model.City) %>
</div>
<div class="editor-field">
<%: Html.TextBoxFor(model => model.City) %>
<%: Html.ValidationMessageFor(model => model.City) %>
</div>
hPot-Tech
53
<div class="editor-label">
<%: Html.LabelFor(model => model.State) %>
</div>
<div class="editor-field">
<%: Html.TextBoxFor(model => model.State) %>
<%: Html.ValidationMessageFor(model => model.State) %>
</div>
<div class="editor-label">
<%: Html.LabelFor(model => model.Code) %>
</div>
<div class="editor-field">
<%: Html.TextBoxFor(model => model.Code) %>
<%: Html.ValidationMessageFor(model => model.Code) %>
</div>
<div class="editor-label">
<%: Html.LabelFor(model => model.Country) %>
</div>
<div class="editor-field">
<%: Html.TextBoxFor(model => model.Country) %>
<%: Html.ValidationMessageFor(model => model.Country) %>
</div>
<div class="editor-label">
<%: Html.LabelFor(model => model.Phone) %>
</div>
<div class="editor-field">
<%: Html.TextBoxFor(model => model.Phone) %>
<%: Html.ValidationMessageFor(model => model.Phone) %>
</div>
<div class="editor-label">
<%: Html.LabelFor(model => model.Website) %>
</div>
hPot-Tech
54
<div class="editor-field">
<%: Html.TextBoxFor(model => model.Website) %>
<%: Html.ValidationMessageFor(model => model.Website) %>
</div>
<div class="editor-label">
<%: Html.LabelFor(model => model.Updated) %>
</div>
<div class="editor-field">
<%: Html.TextBoxFor(model => model.Updated) %> (7/22/2010 8:00 PM)
<%: Html.ValidationMessageFor(model => model.Updated) %>
</div>
<div class="editor-label">
<%: Html.LabelFor(model => model.Description) %>
</div>
<div class="editor-field">
<%: Html.TextBoxFor(model => model.Description) %>
<%: Html.ValidationMessageFor(model => model.Description) %>
</div>
<div class="editor-label">
<%: Html.LabelFor(model => model.Id) %>
</div>
<div class="editor-field">
<%: Html.TextBoxFor(model => model.Id) %>
<%: Html.ValidationMessageFor(model => model.Id) %>
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
<% } %>
hPot-Tech
55
<div>
<%: Html.ActionLink("Back to List", "Index") %>
</div>
</asp:Content>
hPot-Tech
56
Delete.aspx:
57
hPot-Tech
58
Details.aspx
59
<div class="display-label">Updated</div>
<div class="display-field"><%: String.Format("{0:g}", Model.Updated) %></div>
<div class="display-label">Description</div>
<div class="display-field"><%: Model.Description %></div>
<div class="display-label">Type</div>
<div class="display-field"><%: Model.Type %></div>
<div class="display-label">Id</div>
<div class="display-field"><%: Model.Id %></div>
</fieldset>
<p>
<%: Html.ActionLink("Edit", "Edit", new { id=Model.Id }) %> |
<%: Html.ActionLink("Back to List", "Index") %>
</p>
</asp:Content>
hPot-Tech
60
Edit.aspx
61
62
63
</div>
</asp:Content>
hPot-Tech
64
Index.aspx
65
</th>
<th>
Updated
</th>
<th>
Description
</th>
<th>
Type
</th>
<th>
Id
</th>
</tr>
<% foreach (var item in Model) { %>
<tr>
<td>
<%:
<%:
<%:
</td>
<td>
<%:
</td>
<td>
<%:
</td>
<td>
<%:
</td>
<td>
<%:
</td>
<td>
hPot-Tech
66
<%:
</td>
<td>
<%:
</td>
<td>
<%:
</td>
<td>
<%:
</td>
<td>
<%:
</td>
<td>
<%:
</td>
<td>
<%:
</td>
</tr>
item.Country %>
item.Phone %>
item.Website %>
String.Format("{0:g}", item.Updated) %>
item.Description %>
item.Type %>
item.Id %>
<% } %>
</table>
<p>
<%: Html.ActionLink("Create New", "Create") %>
</p>
</asp:Content>
hPot-Tech
67
Web.config
<?xml version="1.0"?>
<!-For more information on how to configure your ASP.NET application, please visit
http://go.microsoft.com/fwlink/?LinkId=152368
-->
<configuration>
<configSections>
<section name="couchbase" type="Couchbase.Configuration.CouchbaseClientSection, Couchbase"/>
</configSections>
<couchbase>
<servers bucket="beer-sample">
<add uri="http://127.0.0.1:8091/pools"/>
</servers>
</couchbase>
<connectionStrings>
<add name="ApplicationServices"
connectionString="data source=.\SQLEXPRESS;Integrated
Security=SSPI;AttachDBFilename=|DataDirectory|aspnetdb.mdf;User Instance=true"
providerName="System.Data.SqlClient" />
</connectionStrings>
hPot-Tech
68
<system.web>
<compilation debug="true" targetFramework="4.0">
<assemblies>
<add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=31BF3856AD364E35" />
<add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=31BF3856AD364E35" />
<add assembly="System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
/>
</assemblies>
</compilation>
<authentication mode="Forms">
<forms loginUrl="~/Account/LogOn" timeout="2880" />
</authentication>
<membership>
<providers>
<clear/>
<add name="AspNetSqlMembershipProvider" type="System.Web.Security.SqlMembershipProvider"
connectionStringName="ApplicationServices"
enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false"
requiresUniqueEmail="false"
maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6"
minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10"
applicationName="/" />
</providers>
</membership>
<profile>
<providers>
<clear/>
<add name="AspNetSqlProfileProvider" type="System.Web.Profile.SqlProfileProvider"
connectionStringName="ApplicationServices" applicationName="/" />
</providers>
</profile>
hPot-Tech
69
<roleManager enabled="false">
<providers>
<clear/>
<add name="AspNetSqlRoleProvider" type="System.Web.Security.SqlRoleProvider"
connectionStringName="ApplicationServices" applicationName="/" />
<add name="AspNetWindowsTokenRoleProvider" type="System.Web.Security.WindowsTokenRoleProvider"
applicationName="/" />
</providers>
</roleManager>
<pages>
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Routing" />
</namespaces>
</pages>
</system.web>
<system.webServer>
<validation validateIntegratedModeConfiguration="false"/>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" />
<bindingRedirect oldVersion="1.0.0.0" newVersion="2.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
hPot-Tech
70
</configuration>
hPot-Tech