In previous post - This is jqMVC# - Definition & Summary, I briefly introduced what is jqMVC#. In this post, I’ll show you a “CNBLOGS Google Tracer” sample application which is applying the jqMVC# architecture.
Function
“Google Tracer” is a HTML & JavaScript application, tracing the blog you are reading with Google Web Search. You should be able to see it running in the left navigation panel in my blog. It is visible only when you are reading any of my blog posts. Technically, it is just googling the title of a post as the search keyword through the Google AJAX Search API.
The function of this application is pretty simple, I believe any of you could implement a similar feature in even half an hour. The points I really want to demonstrate are the benefits from applying the jqMVC# architecture.
MVC Pattern In Script# based JavaScript
Please realize we are writing C# code which is to be compiled info JavaScript by Script# compiler. Like in other server-side MVC pattern implementation, we should have Model, View and Controller.
Models here are value objects (in Script#, they are called Records) representing the search response data. They are strong typed and just matching the JSON response of the Google AJAX Search API.
public sealed class GoogleSearchResponse
{
public GoogleSearchResponseData ResponseData;
public string ResponseDetails;
public int ResponseStatus;
}
[Record]
public sealed class GoogleSearchResponse
{
public GoogleSearchResponseData ResponseData;
public string ResponseDetails;
public int ResponseStatus;
}
[Record]
public sealed class GoogleSearchResponseDataResult
{
[PreserveCase]
public string GsearchResultClass;
public string UnescapedUrl;
public string Url;
public string VisibleUrl;
public string CacheUrl;
public string Title;
public string TitleNoFormatting;
public string Content;
}
…
A view logically wraps the data and events of a UI clip implementation. A view is a bridge clearly separating and connecting the pure UI presentation and the controller. Here we define the view interface first:
{
int SearchStart { get; set; }
string GetSearchKeyword();
void RenderSearchResult(GoogleSearchResponse response);
event DOMEventHandler ShowMoreResults;
}
The view interface could have different implementation, representing different but share the same data and event contracts.
Below is our demo view implementation.
{
private DOMEventHandler _showMoreResults;
private int _searchStart = 0;
#region IGoogleTracerView Members
public int SearchStart
{
get
{
return _searchStart;
}
set
{
_searchStart = value;
}
}
public string GetSearchKeyword()
{
string keyword = (string)(object)JQueryFactory.JQuery(JQuerySelectors.SEARCH_KEYWORD).Text();
return keyword;
}
public void RenderSearchResult(GoogleSearchResponse response)
{
((JTemplatePlugin)JQueryFactory.JQuery(JQuerySelectors.SEARCH_RESULTS_PANEL))
.SetTemplateElement(JTemplateElements.GOOGLE_TRACER)
.ProcessTemplate(response);
JQueryFactory.JQuery(JQuerySelectors.SHOW_MORE_RESULTS_BUTTON).Click(_showMoreResults);
}
public event DOMEventHandler ShowMoreResults
{
add
{
_showMoreResults = (DOMEventHandler)Delegate.Combine(_showMoreResults, value);
}
remove
{
_showMoreResults = (DOMEventHandler)Delegate.Remove(_showMoreResults, value);
}
}
#endregion
}
A controller is responsible for querying data, binding Model and event handlers to View.
{
private IGoogleTracerView _view;
#region Properties
public IGoogleTracerView View
{
get
{
if (_view == null)
_view = (IGoogleTracerView)Container.GetInstance(typeof(IGoogleTracerView));
return _view;
}
}
#endregion
#region Public Methods
public void Execute()
{
View.ShowMoreResults += new DOMEventHandler(ShowMoreResults);
LoadSearchResults();
}
public void ShowMoreResults()
{
View.SearchStart = View.SearchStart + 4;
LoadSearchResults();
}
public static void GoogleWebSearchCallback(object data)
{
((Dictionary)(object)Window.Self)["_googlewebsearchresults"] = data;
}
#endregion
#region Private Methods
private void LoadSearchResults()
{
if (string.IsNullOrEmpty(View.GetSearchKeyword().Trim()))
return;
jQuery.GetScript(
string.Format(
SearchUrls.WEB_SEARCH_URL,
View.GetSearchKeyword().Replace("'", "").Replace(" ", "+").Replace("&", "").Replace("?", ""),
View.SearchStart,
"NIntegrate.Scripts.Test.Demo.GoogleTracer.Controllers.GoogleTracerController.googleWebSearchCallback"
),
(Function)(object)new DOMEventHandler(delegate
{
View.RenderSearchResult((GoogleSearchResponse)((Dictionary)(object)Window.Self)["_googlewebsearchresults"]);
})
);
}
#endregion
}
I use a simple Container which decouples the dependency from the controller to the concrete view implementation in the readonly View property of the controller class.
{
private static Dictionary _cache = new Dictionary();
public static void RegisterInstance(Type type, object instance)
{
_cache[type.FullName] = instance;
}
public static object GetInstance(Type type)
{
return _cache[type.FullName];
}
}
OK, this is all the implementation. Benefited from the MVC pattern, it is well separated and easy to understand, right? But wait, where is the testing?
Test Driven Development (TDD)
To do integration testing, I created a demo HTML page after all the implementation.
<html>
<head>
<title>GoogleTracer Demo</title>
<script type="text/javascript" language="javascript" src="http://www.cnblogs.com/_scripts/jquery-1.3.2.js"></script>
<script type="text/javascript" language="javascript" src="http://www.cnblogs.com/_scripts/jquery-jtemplates.js"></script>
<script type="text/javascript" language="javascript" src="http://www.cnblogs.com/_scripts/sscompat.debug.js"></script>
<script type="text/javascript" language="javascript" src="http://www.cnblogs.com/_scripts/sscorlib.js"></script>
<script type="text/javascript" language="javascript" src="http://www.cnblogs.com/_scripts/ssfx.Core.js"></script>
<script type="text/javascript" language="javascript" src="http://www.cnblogs.com/_scripts/JQuerySharp.debug.js"></script>
<script type="text/javascript" language="javascript" src="http://www.cnblogs.com/_scripts/NIntegrate.Scripts.debug.js"></script>
<script type="text/javascript" language="javascript" src="http://www.cnblogs.com/_scripts/NIntegrate.Scripts.Test.debug.js"></script>
</head>
<body>
<div class="post">
Search keyword: <span class="postTitle">teddyma wcf</span>
</div>
<hr />
<textarea id="jtGoogleTracer" style="display: none">
{#if $T.responseStatus == 200}
{#foreach $T.responseData.results as result}
<a href="{$T.result.url}" title="{$T.result.content.replace('"', '"')}">{$T.result.titleNoFormatting}</a><br />
{#/for}
<b><a href="javascript:void(0)" id="btnShowMoreResults">More>></a></b>
{#else}
Network error, please try again later!
{#/if}
</textarea>
<div id="divSearchResults"></div>
<script type="text/javascript" language="javascript">
NIntegrate.Scripts.Test.Demo.GoogleTracer.Container.registerInstance(
NIntegrate.Scripts.Test.Demo.GoogleTracer.Views.IGoogleTracerView,
new NIntegrate.Scripts.Test.Demo.GoogleTracer.Views.CnblogsGoogleSearchTracerView()
);
new NIntegrate.Scripts.Test.Demo.GoogleTracer.Controllers.GoogleTracerController().execute();
</script>
</body>
</html>
This demo page runs smoothly without any error even the first time. How it achieves? I do TDD.
{
public override void Execute()
{
base.Execute();
CnblogsGoogleSearchTracerView view = new CnblogsGoogleSearchTracerView();
QUnit.Test("Test SearchStart", delegate
{
QUnit.Equals(0, view.SearchStart);
view.SearchStart = 1;
QUnit.Equals(1, view.SearchStart);
view.SearchStart = 2;
QUnit.Equals(2, view.SearchStart);
});
QUnit.Test("Test GetSearchKeyword()", delegate
{
JQueryFactory.JQuery(JQuerySelectors.SEARCH_KEYWORD).Html("keyword1");
QUnit.Equals("keyword1", view.GetSearchKeyword());
JQueryFactory.JQuery(JQuerySelectors.SEARCH_KEYWORD).Html("keyword2");
QUnit.Equals("keyword2", view.GetSearchKeyword());
});
QUnit.Test("Test RenderSearchResult()", delegate
{
jQuery pnlSearchResults = JQueryFactory.JQuery(JQuerySelectors.SEARCH_RESULTS_PANEL);
jQuery btnShowMoreResults = JQueryFactory.JQuery(JQuerySelectors.SHOW_MORE_RESULTS_BUTTON);
Mock mockJQuery = new Mock(Window.Self, "jQuery");
mockJQuery.Modify().Args(JQuerySelectors.SEARCH_RESULTS_PANEL).ReturnValue(pnlSearchResults);
mockJQuery.Modify().Args(JQuerySelectors.SHOW_MORE_RESULTS_BUTTON).ReturnValue(btnShowMoreResults);
Mock mockSetTemplateElement = new Mock(pnlSearchResults, "setTemplateElement");
mockSetTemplateElement.Modify().Args(JTemplateElements.GOOGLE_TRACER).ReturnValue(pnlSearchResults);
Mock mockProcessTemplate = new Mock(pnlSearchResults, "processTemplate");
mockProcessTemplate.Modify().Args(Is.Anything).ReturnValue();
Mock mockShowMoreResultsBindClick = new Mock(btnShowMoreResults, "click");
mockShowMoreResultsBindClick.Modify().Args(Is.Anything).ReturnValue();
view.RenderSearchResult(new GoogleSearchResponse());
mockJQuery.Verify();
mockJQuery.Restore();
mockSetTemplateElement.Verify();
mockSetTemplateElement.Restore();
mockProcessTemplate.Verify();
mockProcessTemplate.Restore();
mockShowMoreResultsBindClick.Verify();
mockShowMoreResultsBindClick.Restore();
});
QUnit.Test("Test ShowMoreResults Event", delegate
{
QUnit.Equals(false, _showMoreResultsClicked);
view.ShowMoreResults += new System.DHTML.DOMEventHandler(view_ShowMoreResults);
view.RenderSearchResult(new GoogleSearchResponse());
JQueryFactory.JQuery(JQuerySelectors.SHOW_MORE_RESULTS_BUTTON).Click();
QUnit.Equals(true, _showMoreResultsClicked);
});
}
private bool _showMoreResultsClicked = false;
void view_ShowMoreResults()
{
_showMoreResultsClicked = true;
}
}
{
private int _searchStart = 0;
#region IGoogleTracerView Members
public int SearchStart
{
get
{
return _searchStart;
}
set
{
_searchStart = value;
}
}
public string GetSearchKeyword()
{
return "keyword";
}
public void RenderSearchResult(NIntegrate.Scripts.Test.Demo.GoogleTracer.Records.GoogleSearchResponse response)
{
return;
}
public event System.DHTML.DOMEventHandler ShowMoreResults;
#endregion
}
public class GoogleTracerControllerTest : TestCase
{
public override void Execute()
{
base.Execute();
MockGoogleTracerView mockView = new MockGoogleTracerView();
Container.RegisterInstance(typeof(IGoogleTracerView), mockView);
GoogleTracerController controller = new GoogleTracerController();
QUnit.Test("Test get View", delegate
{
QUnit.Equals(mockView, controller.View);
});
QUnit.Test("Test Execute() & ShowMoreResults()", delegate
{
GoogleSearchResponse data = new GoogleSearchResponse();
Mock mockAddShowMoreResults = new Mock(mockView, "add_showMoreResults");
mockAddShowMoreResults.Modify().Args(Is.Anything).ReturnValue();
Mock mockRenderSearchResult = new Mock(mockView, "renderSearchResult");
mockRenderSearchResult.Modify().Args(data).ReturnValue();
mockRenderSearchResult.Modify().Args(data).ReturnValue();
Mock mockGetScript = new Mock(Script.Eval("jQuery"), "getScript");
mockGetScript.Modify().Args(Is.Anything, Is.Anything).Callback(1, null).ReturnValue();
mockGetScript.Modify().Args(Is.Anything, Is.Anything).Callback(1, null).ReturnValue();
QUnit.Equals(0, mockView.SearchStart);
((Dictionary)(object)Window.Self)["_googlewebsearchresults"] = data;
controller.Execute();
((Dictionary)(object)Window.Self)["_googlewebsearchresults"] = data;
controller.ShowMoreResults();
QUnit.Equals(4, mockView.SearchStart);
mockAddShowMoreResults.Verify();
mockAddShowMoreResults.Restore();
mockRenderSearchResult.Verify();
mockRenderSearchResult.Restore();
mockGetScript.Verify();
mockGetScript.Restore();
});
}
}
The testing results of QUnit:
Source Code
You could download the latest source code of this demo from SVN: http://nintegrate.googlecode.com/svn/trunk/jqMVCSharp/
or download this zip file: jqMVCSharpDemo.zip
To open the project files in Visual Studio 2008, you should install Script# 0.5.6 for VS 2008 first.