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.
Google Tracer Records
[Record]
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:
IGoogleTracerView
public
interface
IGoogleTracerView
{
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.
CnblogsGoogleSearchTracerView
public
class
CnblogsGoogleSearchTracerView : IGoogleTracerView
{
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.
GoogleTracerController
public
class
GoogleTracerController
{
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.
Container
public
static
class
Container
{
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.
GoogleTracer Demo HTML
<!
DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"
>
<
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.
CnblogsGoogleSearchTracerViewTest
public
class
CnblogsGoogleSearchTracerViewTest : TestCase
{
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
;
}
}
GoogleTracerControllerTest
public
class
MockGoogleTracerView : IGoogleTracerView
{
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.