Publié par : yoannr | 1 novembre 2011

Introduction à CQRS/ES

CQRS est un acronyme pour Command Query Responsibility Segregation.C’est à dire la séparation de  responsabilité des commandes et des query. Le principe est très simple et peut être enonce comme : Poser une question ne doit pas changer la réponse.  

Voici à quoi ressemble une application classique :
application classique

Le domaine va  devoir gérer aussi bien l’entrée d’information que la sortie. Or comme vous le savez, les requêtes de sorties sont souvent quelque chose de compliqué. Assez souvent on a besoin d’utiliser et de créer des objects qui ne serviront qu’a un usage bien particulier. Or le domaine doit gérer ces particularismes et les intégrer dans ses pérégrinations.

C’est la qu’intervient CQRS, en séparant les commandes des query on arrive a un schema plus simple car bien delimite :
Command sous CQRS

Le client envoie une commande (l’équivalent du DTO du schema classique) à l’application. Cette command correspond a un fonction void. Si je prends l’exemple d’une application twitter, on pourrait avoir une comand comme ceci :

 public class PostNewTweetCommand : CommandBase
    {
        public string Message { get; set; }
        public string Who { get; set; }
    }
L’application reçoit cette Command et sait la traiter. En fonction des règles de validation et de disponibilité de l’infrastructure (réseau, ..) on peut imaginer que cette action sera un succès ou entraînera une exception. Au client de gérer cet état de fait.
Le cote query est quant à lui gérer séparement via une interface très légère qui s’occupe d’optimiser les query.

CQRS coté query

La database est attaquée directement par la couche mince d’accès aux données comme par exemple linq2sql ou  autre chose d’encore plus léger, et permet ainsi une optimisation des query sans alourdir le cote command.

Credits pour les exemples et les images :

www.dddcqrs.com par Greg Young

le projet framework nCQRS


Publié par : yoannr | 26 septembre 2011

FlexBox according to my needs

In order to share some tought, this post will be in english , sorry for french readers.

I needed to change the Jquery Flexbox to get a select that could be filtered like a textbox without removing any items from the list if the little arrow on the right was clicked.

I had to change the FlexBox Jquery script in order to get this. (full source at the end of this article if the link does not work anymore)

 

 

And then I packed it up in an usercontrol ASCX in order to ghet my control ready for my use :

page aspx :

<%@ControlLanguage=”C#”AutoEventWireup=”true”CodeBehind=”AutoCompleteDropDownList.ascx.cs”

    Inherits=”Super.Web.userControl.AutoCompleteDropDownList”%>

 

<divrunat=serverid=C_DDL_AutoComplete></div>

 

<scripttype=”text/javascript”>

 

    jQuery.noConflict()(function($){

        $(document).ready(function() {

            $(‘#<%= C_DDL_AutoComplete.ClientID  %>).flexbox({

                “results”: <%= JsonDataSource %>,

                 “total”: <%= JsonDataSourceTotal %>

            }, {

                watermark : <%= WaterMark %>,

                width: <%= Width %>,

                allowInput: <%= AutoComplete.ToString().ToLower() %>,

                paging : false,

                onSelect: function() { $(‘#<%= C_Hid_SelectedValue.ClientID %>).val(this.value);}  //onselect

               }); //flexbox

             var idSelected = $(‘#<%= C_Hid_SelectedValue.ClientID %>).val();

             $(‘#<%= C_DDL_AutoComplete.ClientID  %>).setIdValue(idSelected);

 

             Data= <%= JsonDataSource %>;

             DataJson = $.parseJSON(Data);

             for(index=0;index<DataJson.length;index++)

             {

                if(DataJson[index].id==idSelected)

                {

                    $(‘#<%= C_DDL_AutoComplete.ClientID  %>).setValue(DataJson[index].name);

                    break; ;

                }

             }

            

            

        });//ready

       

    });//No conflict

  

</script>

 

<asp:HiddenFieldrunat=serverID=C_Hid_SelectedValue/>

and its code behind :

using System;

using System.Collections.Generic;

using System.Linq;

using System.Web;

using System.Web.UI;

using System.Web.UI.WebControls;

using Super.Core;

 

namespace Super.Web.userControl

{

 

    publicpartialclassAutoCompleteDropDownList : UserControl

    {

        privateint _Width = 350;

        privatebool _AutoComplete = true;

 

        protectedstring JsonDataSource;

        protectedstring JsonDataSourceTotal;

 

 

        public  IEnumerable<EntitaVisualizzazione> DataSource;

        ///<summary>

        /// [{ "id": "myid" , "name" : "myName" },

        /// { "id": "myid2" , "name" : "myName2" },

        /// { "id": "myid3" , "name" : "myName3" }]

        ///</summary>

        publicstring DataSourceJsonClient { get; set; }

        ///<summary>

        /// Number of elements provided in DataSourceJsonClient

        ///</summary>

        publicint? DataSourceJsonClientTotal { get; set; }

        publicint Width

        {

            get { return _Width; }

            set { _Width = value; }

        }

        publicbool AutoComplete

        {

            get { return _AutoComplete; }

            set { _AutoComplete = value; }

        }

        publicstring WaterMark { get; set; }

        publicstring SelectedValue

        {

            get { return C_Hid_SelectedValue.Value; }

            set { C_Hid_SelectedValue.Value=value; }

        }

 

 

        protectedoverridevoid OnPreRender(EventArgs e)

        {

            base.OnPreRender(e);

 

            JsonDataSource = string.Empty;

            if (!string.IsNullOrEmpty(DataSourceJsonClient) && DataSource != null)

            {

                thrownewException(“DataSourceJson and DataSource cannot be filled at the same time”);

            }

 

 

            if (!string.IsNullOrEmpty(DataSourceJsonClient))

            {

                if (!DataSourceJsonClientTotal.HasValue)

                    thrownewException(“DataSourceJsonClientTotal must have a value, when DataSourceJsonClient is filled”);

 

                if (!string.IsNullOrEmpty(SelectedValue))

                {

                    if (!DataSourceJsonClient.ToLower().Contains(string.Format(” id : ‘{0}’”,SelectedValue).ToLower()))

                        thrownewException(string.Format(“The SelectedValue ‘{0}’ was not found in the Items of the DataSourceJsonClient”, SelectedValue));

                }

 

                JsonDataSource = DataSourceJsonClient;

                JsonDataSourceTotal = DataSourceJsonClientTotal.Value.ToString();

            }

            else

            {

                if (DataSource != null)

                {

                    if (!string.IsNullOrEmpty(SelectedValue))

                    {

                        if (!DataSource.Any(x => x.Id == SelectedValue))

                            thrownewException(string.Format(“The SelectedValue ‘{0}’ was not found in the Items of the Datasource”, SelectedValue));

                    }

 

                    JsonDataSource = “[";

                    foreach (IEntitaVisualizzazione entita in DataSource)

                    {

                        JsonDataSource += string.Format(@"{{ ""id"": ""{0}"", ""name"": ""{1}"" }},", entita.Id, entita.Descrizione);

                    }

                    if (!string.IsNullOrEmpty(JsonDataSource))

                        JsonDataSource = JsonDataSource.Substring(0, JsonDataSource.Length - 1);

 

                    JsonDataSource += "]“;

 

                    JsonDataSourceTotal = DataSource.Count().ToString();

 

                }

            }

           

        }

    }

}

and here is my final use in a standard aspx page :

<Super:AutoCompleteDropDownList runat=”server” ID=”C_DDL_Lotti”
DataSource=”<%# Entities %>”></Super:AutoCompleteDropDownList>

and its code behind :

protected IEnumerable<MyEntity> Entities;

Entities = GetMyListFromDatabse()
.Select(x => new MyEntity()
{
Id = x.Id.ToString(),
Descrizione = x.DescrizioneComplete
});

 

 

FlexBox Jquery script :

/*!

* jQuery FlexBox $Version: 0.9.6 $

*

* Copyright (c) 2008-2010 Noah Heldman and Fairway Technologies (http://www.fairwaytech.com/flexbox)

* Licensed under Ms-PL (http://www.codeplex.com/flexbox/license)

*

* $Date: 2010-11-24 01:02:00 PM $

* $Rev: 0.9.6.1 $

*/

(function ($) {

$.flexbox = function (div, o) {

 

// TODO: in straight type-ahead mode (showResults: false), if noMatchingResults, dropdown appears after new match

// TODO: consider having options.mode (select, which replaces html select; combobox; suggest; others?)

// TODO: on resize (at least when wrapping within a table), the arrow is pushed down to the next line

// TODO: check for boundary/value problems (such as minChars of -1) and alert them

// TODO: add options for advanced paging template

// TODO: general cleanup and refactoring, commenting

// TODO: detailed Exception handling, logging

// TODO: FF2, up arrow from bottom has erratic scroll behavior (if multiple flexboxes on page)

// TODO: FF2 (and maybe IE7): if maxVisibleRows == number of returned rows, height is a bit off (maybe set to auto?)

// TODO: escape key only works from input box (this might be okay)

// TODO: make .getJSON parameters (object and callback function) configurable (e.g. when calling yahoo image search)

// TODO: escape key reverts to previous value (FF only?) (is this a good thing?)

 

// TEST: highlightMatches uses the case of whatever you typed in to replace the match string, which can look funny

// TEST: handle pageDown and pageUp keys when scrolling through results

// TEST: allow client-side paging (return all data initially, set paging:{pageSize:#}, and ensure maxCacheBytes is > 0)

// TEST: accept json object as first parameter to flexbox instead of page source, and have it work like a combobox

// TEST: implement no results template

// TEST: implement noResultsText and class

// TEST: watermark color should be configurable (and so should default input color)

// TEST: exception handling and alerts for common mistakes

// TEST: first example should use defaults ONLY

// TEST: add property initialValue, so you can set it when the flexbox loads

// TEST: handle hidden input value for form submissions

// TEST: how can we allow programmatically setting the field value (and therefore hidden value).  add jquery function?

// TEST: use pageSize parameter as threshold to switch from no paging to paging based on results

// TEST: if you type in an input value that matches the html, it might display html code (try typing “class” in the input box)

// TEST: don’t require all paging subprops (let default override)

// TEST: when tabbing from one ffb to another, the previous ffb results flash…

// TEST: IE7: when two non-paging ffbs right after each other, with only a clear-both div between them, the bottom ffb jumps down when selecting a value, then jumps back up on mouseover

// TEST: FF2, make sure we scroll to top before showing results (maxVisibleRows only)

// TEST: if maxVisibleRows is hiding the value the user types in to the input, scroll to that value (is this even possible?)

// TEST: make sure caching supports multiple ffbs uniquely

// TEST: when entering a number in the paging input box, the results are displayed twice

 

var timeout = false,      // hold timeout ID for suggestion results to appear

cache = [],                   // simple array with cacheData key values, MRU is the first element

cacheData = [],         // associative array holding actual cached data

cacheSize = 0,                // size of cache in bytes (cache up to o.maxCacheBytes bytes)

delim = ‘\u25CA’,       // use an obscure unicode character (lozenge) as the cache key delimiter

scrolling = false,

pageSize = o.paging && o.paging.pageSize ? o.paging.pageSize : 0,

retrievingRemoteData = false,

$div = $(div).css(‘position’, ‘relative’).css(‘z-index’, 0);

 

// The hiddenField MUST be appended to the div before the input, or IE7 does not shift the dropdown below the input field (it overlaps)

var $hdn = $(‘<input type=”hidden”/>’)

.attr(‘id’, $div.attr(‘id’) + ‘_hidden’)

.attr(‘name’, $div.attr(‘id’))

.val(o.initialValue)

.appendTo($div);

var $input = $(‘<input/>’)

.attr(‘id’, $div.attr(‘id’) + ‘_input’)

.attr(‘autocomplete’, ‘off’)

.addClass(o.inputClass)

.css(‘width’, o.width + ‘px’)

.appendTo($div)

.click(function (e) {

if (o.watermark !== ” && this.value === o.watermark)

this.value = ”;

else

this.select();

})

.focus(function (e) {

$(this).removeClass(‘watermark’);

})

.blur(function (e) {

if (this.value === ”) $hdn.val(”);

setTimeout(function () { if (!$input.data(‘active’)) hideResults(); }, 200);

})

.keydown(processKeyDown);

 

if (o.initialValue !== ”)

$input.val(o.initialValue).removeClass(‘watermark’);

else

$input.val(o.watermark).addClass(‘watermark’);

 

var arrowWidth = 0;

if (o.showArrow && o.showResults) {

var arrowClick = function () {

if ($ctr.is(‘:visible’)) {

hideResults();

}

else {

$input.focus();

if (o.watermark !== ” && $input.val() === o.watermark)

$input.val(”);

else

$input.select();

if (timeout)

clearTimeout(timeout);

timeout = setTimeout(function () { flexbox(1, true, o.arrowQuery); }, o.queryDelay);

}

};

var $arrow = $(‘<span></span>’)

.attr(‘id’, $div.attr(‘id’) + ‘_arrow’)

.addClass(o.arrowClass)

.addClass(‘out’)

.hover(function () {

$(this).removeClass(‘out’).addClass(‘over’);

}, function () {

$(this).removeClass(‘over’).addClass(‘out’);

})

.mousedown(function () {

$(this).removeClass(‘over’).addClass(‘active’);

})

.mouseup(function () {

$(this).removeClass(‘active’).addClass(‘over’);

})

.click(arrowClick)

.appendTo($div);

arrowWidth = $arrow.width();

$input.css(‘width’, (o.width – arrowWidth) + ‘px’);

}

if (o.selectBehavior)

{ o.selectFirstMatch = false; $input.click(arrowClick); } // simulate <select> behavior

 

// Handle presence of CSS Universal Selector (*) that defines padding by verifying what the browser thinks the outerHeight is.

// In FF, the outerHeight() will not pick up the correct input field padding

var inputPad = $input.outerHeight() – $input.height() – 2;

var inputWidth = $input.outerWidth() – 2;

var top = $input.outerHeight();

 

if (inputPad === 0) {

inputWidth += 4;

top += 4;

}

else if (inputPad !== 4) {

inputWidth += inputPad;

top += inputPad;

}

 

var $ctr = $(‘<div></div>’)

.attr(‘id’, $div.attr(‘id’) + ‘_ctr’)

.css(‘width’, inputWidth + arrowWidth)

.css(‘top’, top)

.css(‘left’, 0)

.addClass(o.containerClass)

.appendTo($div)

.mousedown(function (e) {

$input.data(‘active’, true);

})

.hide();

 

var $content = $(‘<div></div>’)

.addClass(o.contentClass)

.appendTo($ctr)

.scroll(function () {

scrolling = true;

});

 

var $paging = $(‘<div></div>’).appendTo($ctr);

$div.css(‘height’, $input.outerHeight());

 

function processKeyDown(e) {

// handle modifiers

var mod = 0;

if (typeof (e.ctrlKey) !== ‘undefined’) {

if (e.ctrlKey) mod |= 1;

if (e.shiftKey) mod |= 2;

} else {

if (e.modifiers & Event.CONTROL_MASK) mod |= 1;

if (e.modifiers & Event.SHIFT_MASK) mod |= 2;

}

// if the keyCode is one of the modifiers, bail out (we’ll catch it on the next keypress)

if (/16$|17$/.test(e.keyCode)) return; // 16 = Shift, 17 = Ctrl

 

var tab = e.keyCode === 9, esc = e.keyCode === 27;

var tabWithModifiers = e.keyCode === 9 && mod > 0;

var backspace = e.keyCode === 8; // we will end up extending the delay time for backspaces…

 

// tab is a special case, since we want to bubble events…

if (tab) if (getCurr()) selectCurr();

 

// handling up/down/escape/right arrow/left arrow requires results to be visible

// handling enter requires that AND a result to be selected

if ((/27$|38$|33$|34$/.test(e.keyCode) && $ctr.is(‘:visible’)) ||

(/13$|40$/.test(e.keyCode)) || !o.allowInput) {

 

if (e.preventDefault) e.preventDefault();

if (e.stopPropagation) e.stopPropagation();

 

e.cancelBubble = true;

e.returnValue = false;

 

switch (e.keyCode) {

case 38: // up arrow

prevResult();

break;

case 40: // down arrow

if ($ctr.is(‘:visible’)) nextResult();

else flexboxDelay(true);

break;

case 13: // enter

if (getCurr()) selectCurr();

else flexboxDelay(true);

break;

case 27: //   escape

hideResults();

break;

case 34: // page down

if (!retrievingRemoteData) {

if (o.paging) $(‘#’ + $div.attr(‘id’) + ‘n’).click();

else nextPage();

}

break;

case 33: // page up

if (!retrievingRemoteData) {

if (o.paging) $(‘#’ + $div.attr(‘id’) + ‘p’).click();

else prevPage();

}

break;

default:

if (!o.allowInput) { return; }

}

} else if (!esc && !tab && !tabWithModifiers) { // skip esc and tab key and any modifiers

flexboxDelay(false, backspace);

}

}

 

function flexboxDelay(simulateArrowClick, increaseDelay) {

if (timeout) clearTimeout(timeout);

var delay = increaseDelay ? o.queryDelay * 5 : o.queryDelay;

timeout = setTimeout(function () { flexbox(1, simulateArrowClick, ”); }, delay);

}

 

function flexbox(p, arrowOrPagingClicked, prevQuery) {

if (arrowOrPagingClicked) prevQuery = ”;

var q = prevQuery && prevQuery.length > 0 ? prevQuery : $.trim($input.val());

 

if (q.length >= o.minChars || arrowOrPagingClicked) {

// If we are getting data from the server, set the height of the content box so it doesn’t shrink when navigating between pages, due to the $content.html(”) below…

if ($content.outerHeight() > 0)

$content.css(‘height’, $content.outerHeight());

$content.html(”).attr(‘scrollTop’, 0);

 

var cached = checkCache(q, p);

 

if ((cached && !arrowOrPagingClicked && o.selectBehavior) || (cached && !o.selectBehavior)) {

$content.css(‘height’, ‘auto’);

displayItems(cached.data, q);

showPaging(p, cached.t);

}

else {

var params = { q: q, p: p, s: pageSize, contentType: ‘application/json; charset=utf-8′ };

var callback = function (data, overrideQuery) {

if (overrideQuery === true) q = overrideQuery; // must compare to boolean because by default, the string value “success” is passed when the jQuery $.getJSON method’s callback is called

var totalResults = parseInt(data[o.totalProperty]);

 

// Handle client-side paging, if any paging configuration options were specified

if (isNaN(totalResults) && o.paging) {

if (o.maxCacheBytes <= 0) alert(‘The “maxCacheBytes” configuration option must be greater\nthan zero when implementing client-side paging.’);

totalResults = data[o.resultsProperty].length;

 

var pages = totalResults / pageSize;

if (totalResults % pageSize > 0) pages = parseInt(++pages);

 

for (var i = 1; i <= pages; i++) {

var pageData = {};

pageData[o.totalProperty] = totalResults;

pageData[o.resultsProperty] = data[o.resultsProperty].splice(0, pageSize);

if (i === 1) totalSize = displayItems(pageData, q);

updateCache(q, i, pageSize, totalResults, pageData, totalSize);

}

}

else {

var totalSize = displayItems(data, q);

updateCache(q, p, pageSize, totalResults, data, totalSize);

}

showPaging(p, totalResults);

$content.css(‘height’, ‘auto’);

retrievingRemoteData = false;

};

if (typeof (o.source) === ‘object’) {

if ((!o.allowInput || o.selectBehavior) && arrowOrPagingClicked)

callback(o.source);

else

callback(filter(o.source, params));

 

}

else {

retrievingRemoteData = true;

if (o.method.toUpperCase() == ‘POST’) $.post(o.source, params, callback, ‘json’);

else $.getJSON(o.source, params, callback);

}

}

} else

hideResults();

}

 

function filter(data, params) {

var filtered = {};

filtered[o.resultsProperty] = [];

filtered[o.totalProperty] = 0;

var index = 0;

 

for (var i = 0; i < data[o.resultsProperty].length; i++) {

var indexOfMatch = data[o.resultsProperty][i][o.displayValue].toLowerCase().indexOf(params.q.toLowerCase());

if ((o.matchAny && indexOfMatch !== -1) || (!o.matchAny && indexOfMatch === 0)) {

filtered[o.resultsProperty][index++] = data[o.resultsProperty][i];

filtered[o.totalProperty] += 1;

}

}

if (o.paging) {

var start = (params.p – 1) * params.s;

var howMany = (start + params.s) > filtered[o.totalProperty] ? filtered[o.totalProperty] – start : params.s;

filtered[o.resultsProperty] = filtered[o.resultsProperty].splice(start, howMany);

}

return filtered;

}

 

function showPaging(p, totalResults) {

$paging.html(”).removeClass(o.paging.cssClass); // clear out for threshold scenarios

if (o.showResults && o.paging && totalResults > pageSize) {

var pages = totalResults / pageSize;

if (totalResults % pageSize > 0) pages = parseInt(++pages);

outputPagingLinks(pages, p, totalResults);

}

}

 

function handleKeyPress(e, page, totalPages) {

if (/^13$|^39$|^37$/.test(e.keyCode)) {

if (e.preventDefault)

e.preventDefault();

if (e.stopPropagation)

e.stopPropagation();

 

e.cancelBubble = true;

e.returnValue = false;

 

switch (e.keyCode) {

case 13: // Enter

if (/^\d+$/.test(page) && page > 0 && page <= totalPages)

flexbox(page, true);

else

alert(‘Please enter a page number between 1 and ‘ + totalPages);

// TODO: make this alert a function call, and a customizable parameter

break;

case 39: // right arrow

$(‘#’ + $div.attr(‘id’) + ‘n’).click();

break;

case 37: // left arrow

$(‘#’ + $div.attr(‘id’) + ‘p’).click();

break;

}

}

}

 

function handlePagingClick(e) {

flexbox(parseInt($(this).attr(‘page’)), true, $input.attr(‘pq’)); // pq == previous query

return false;

}

 

function outputPagingLinks(totalPages, currentPage, totalResults) {

// TODO: make these configurable images

var first = ‘&lt;&lt;’,

prev = ‘&lt;’,

next = ‘&gt;’,

last = ‘&gt;&gt;’,

more = ‘…’;

 

$paging.addClass(o.paging.cssClass);

 

// set up our base page link element

var $link = $(‘<a/>’)

.attr(‘href’, ‘#’)

.addClass(‘page’)

.click(handlePagingClick),

$span = $(‘<span></span>’).addClass(‘page’),

divId = $div.attr(‘id’);

 

// show first page

if (currentPage > 1) {

$link.clone(true).attr(‘id’, divId + ‘f’).attr(‘page’, 1).html(first).appendTo($paging);

$link.clone(true).attr(‘id’, divId + ‘p’).attr(‘page’, currentPage – 1).html(prev).appendTo($paging);

}

else {

$span.clone(true).html(first).appendTo($paging);

$span.clone(true).html(prev).appendTo($paging);

}

 

if (o.paging.style === ‘links’) {

var maxPageLinks = o.paging.maxPageLinks;

// show page numbers

if (totalPages <= maxPageLinks) {

for (var i = 1; i <= totalPages; i++) {

if (i === currentPage) {

$span.clone(true).html(currentPage).appendTo($paging);

}

else {

$link.clone(true).attr(‘page’, i).html(i).appendTo($paging);

}

}

}

else {

if ((currentPage + parseInt(maxPageLinks / 2)) > totalPages) {

startPage = totalPages – maxPageLinks + 1;

}

else {

startPage = currentPage – parseInt(maxPageLinks / 2);

}

 

if (startPage > 1) {

$link.clone(true).attr(‘page’, startPage – 1).html(more).appendTo($paging);

}

else {

startPage = 1;

}

 

for (var i = startPage; i < startPage + maxPageLinks; i++) {

if (i === currentPage) {

$span.clone(true).html(i).appendTo($paging);

}

else {

$link.clone(true).attr(‘page’, i).html(i).appendTo($paging);

}

}

 

if (totalPages > (startPage + maxPageLinks)) {

$link.clone(true).attr(‘page’, i).html(more).appendTo($paging);

}

}

}

else if (o.paging.style === ‘input’) {

var $pagingBox = $(‘<input/>’)

.addClass(‘box’)

.click(function (e) {

this.select();

})

.keypress(function (e) {

return handleKeyPress(e, this.value, totalPages);

})

.val(currentPage)

.appendTo($paging);

}

 

if (currentPage < totalPages) {

$link.clone(true).attr(‘id’, divId + ‘n’).attr(‘page’, +currentPage + 1).html(next).appendTo($paging);

$link.clone(true).attr(‘id’, divId + ‘l’).attr(‘page’, totalPages).html(last).appendTo($paging);

}

else {

$span.clone(true).html(next).appendTo($paging);

$span.clone(true).html(last).appendTo($paging);

}

var startingResult = (currentPage – 1) * pageSize + 1;

var endingResult = (startingResult > (totalResults – pageSize)) ? totalResults : startingResult + pageSize – 1;

 

if (o.paging.showSummary) {

var summaryData = {

“start”: startingResult,

“end”: endingResult,

“total”: totalResults,

“page”: currentPage,

“pages”: totalPages

};

var html = o.paging.summaryTemplate.applyTemplate(summaryData);

$(‘<br/>’).appendTo($paging);

$(‘<span></span>’)

.addClass(o.paging.summaryClass)

.html(html)

.appendTo($paging);

}

}

 

function checkCache(q, p) {

var key = q + delim + p; // use null character as delimiter

if (cacheData[key]) {

for (var i = 0; i < cache.length; i++) { // TODO: is it possible to not loop here?

if (cache[i] === key) {

// pull out the matching element (splice), and add it to the beginning of the array (unshift)

cache.unshift(cache.splice(i, 1)[0]);

return cacheData[key];

}

}

}

return false;

}

 

function updateCache(q, p, s, t, data, size) {

if (o.maxCacheBytes > 0) {

while (cache.length && (cacheSize + size > o.maxCacheBytes)) {

var cached = cache.pop();

cacheSize -= cached.size;

}

var key = q + delim + p; // use null character as delimiter

cacheData[key] = {

q: q,

p: p,

s: s,

t: t,

size: size,

data: data

}; // add the data to the cache at the hash key location

cache.push(key); // add the key to the MRU list

cacheSize += size;

}

}

 

function displayItems(d, q) {

var totalSize = 0, itemCount = 0;

 

if (!d)

return;

 

$hdn.val($input.val());

if (parseInt(d[o.totalProperty]) === 0 && o.noResultsText && o.noResultsText.length > 0) {

$content.addClass(o.noResultsClass).html(o.noResultsText);

$ctr.show();

return;

} else $content.removeClass(o.noResultsClass);

 

for (var i = 0; i < d[o.resultsProperty].length; i++) {

var data = d[o.resultsProperty][i],

result = o.resultTemplate.applyTemplate(data),

exactMatch = q === result,

selectedMatch = false,

hasHtmlTags = false,

match = data[o.displayValue];

 

if (!exactMatch && o.highlightMatches && q !== ”) {

var pattern = q,

highlightStart = match.toLowerCase().indexOf(q.toLowerCase()),

replaceString = ‘<span>’ + match.substr(highlightStart, q.length) + ‘</span>’;

if (result.match(‘<(.|\n)*?>’)) { // see if the content contains html tags

hasHtmlTags = true;

pattern = ‘(>)([^<]*?)(‘ + q + ‘)((.|\n)*?)(<)’; // TODO: look for a better way

replaceString = ‘$1$2<span>$3</span>$4$6′;

}

result = result.replace(new RegExp(pattern, o.highlightMatchesRegExModifier), replaceString);

}

 

// write the value of the first match to the input box, and select the remainder,

// but only if autoCompleteFirstMatch is set, and there are no html tags in the response

if (o.autoCompleteFirstMatch && !hasHtmlTags && i === 0) {

if (q.length > 0 && match.toLowerCase().indexOf(q.toLowerCase()) === 0) {

$input.attr(‘pq’, q); // pq == previous query

$hdn.val(data[o.hiddenValue]);

$input.val(data[o.displayValue]);

selectedMatch = selectRange(q.length, $input.val().length);

}

}

 

if (!o.showResults) return;

 

$row = $(‘<div></div>’)

.attr(‘id’, data[o.hiddenValue])

.attr(‘val’, data[o.displayValue])

.addClass(‘row’)

.html(result)

.appendTo($content);

 

if (exactMatch || (++itemCount == 1 && o.selectFirstMatch) || selectedMatch) {

$row.addClass(o.selectClass);

}

totalSize += result.length;

}

 

if (totalSize === 0) {

hideResults();

return;

}

 

$ctr.parent().css(‘z-index’, 11000);

$ctr.show();

 

$content

.children(‘div’)

.mouseover(function () {

$content.children(‘div’).removeClass(o.selectClass);

$(this).addClass(o.selectClass);

})

.mouseup(function (e) {

e.preventDefault();

e.stopPropagation();

selectCurr();

});

 

if (o.maxVisibleRows > 0) {

var maxHeight = $row.outerHeight() * o.maxVisibleRows;

$content.css(‘max-height’, maxHeight);

}

 

return totalSize;

}

 

function selectRange(s, l) {

var tb = $input[0];

if (tb.createTextRange) {

var r = tb.createTextRange();

r.moveStart(‘character’, s);

r.moveEnd(‘character’, l – tb.value.length);

r.select();

} else if (tb.setSelectionRange) {

tb.setSelectionRange(s, l);

}

tb.focus();

return true;

}

 

String.prototype.applyTemplate = function (d) {

try {

if (d === ”) return this;

return this.replace(/{([^{}]*)}/g,

function (a, b) {

var r;

if (b.indexOf(‘.’) !== -1) { // handle dot notation in {}, such as {Thumbnail.Url}

var ary = b.split(‘.’);

var obj = d;

for (var i = 0; i < ary.length; i++)

obj = obj[ary[i]];

r = obj;

}

else

r = d[b];

if (typeof r === ‘string’ || typeof r === ‘number’) return r; else throw (a);

}

);

} catch (ex) {

alert(‘Invalid JSON property ‘ + ex + ‘ found when trying to apply resultTemplate or paging.summaryTemplate.\nPlease check your spelling and try again.’);

}

};

 

function hideResults() {

$input.data(‘active’, false); // for input blur

$div.css(‘z-index’, 0);

$ctr.hide();

}

 

function getCurr() {

if (!$ctr.is(‘:visible’))

return false;

 

var $curr = $content.children(‘div.’ + o.selectClass);

 

if (!$curr.length)

$curr = false;

 

return $curr;

}

 

function selectCurr() {

$curr = getCurr();

 

if ($curr) {

$hdn.val($curr.attr(‘id’));

$input.val($curr.attr(‘val’)).focus();

hideResults();

 

if (o.onSelect) {

o.onSelect.apply($hdn[0]);

}

}

}

 

function supportsGetBoxObjectFor() {

try {

document.getBoxObjectFor(document.body);

return true;

}

catch (e) {

return false;

}

}

 

function supportsGetBoundingClientRect() {

try {

document.body.getBoundingClientRect();

return true;

}

catch (e) {

return false;

}

}

 

function nextPage() {

$curr = getCurr();

 

if ($curr && $curr.next().length > 0) {

$curr.removeClass(o.selectClass);

 

for (var i = 0; i < o.maxVisibleRows; i++) {

if ($curr.next().length > 0) {

$curr = $curr.next();

}

}

 

$curr.addClass(o.selectClass);

var scrollPos = $content.attr(‘scrollTop’);

$content.attr(‘scrollTop’, scrollPos + $content.height());

}

else if (!$curr)

$content.children(‘div:first-child’).addClass(o.selectClass);

}

 

function prevPage() {

$curr = getCurr();

 

if ($curr && $curr.prev().length > 0) {

$curr.removeClass(o.selectClass);

 

for (var i = 0; i < o.maxVisibleRows; i++) {

if ($curr.prev().length > 0) {

$curr = $curr.prev();

}

}

 

$curr.addClass(o.selectClass);

var scrollPos = $content.attr(‘scrollTop’);

$content.attr(‘scrollTop’, scrollPos – $content.height());

}

else if (!$curr)

$content.children(‘div:last-child’).addClass(o.selectClass);

}

 

function nextResult() {

$curr = getCurr();

 

if ($curr && $curr.next().length > 0) {

$curr.removeClass(o.selectClass).next().addClass(o.selectClass);

var scrollPos = $content.attr(‘scrollTop’),

curr = $curr[0], parentBottom, bottom, height;

if (supportsGetBoxObjectFor()) {

parentBottom = document.getBoxObjectFor($content[0]).y + $content.attr(‘offsetHeight’);

bottom = document.getBoxObjectFor(curr).y + $curr.attr(‘offsetHeight’);

height = document.getBoxObjectFor(curr).height;

}

else if (supportsGetBoundingClientRect()) {

parentBottom = $content[0].getBoundingClientRect().bottom;

var rect = curr.getBoundingClientRect();

bottom = rect.bottom;

height = bottom – rect.top;

}

if (bottom >= parentBottom)

$content.attr(‘scrollTop’, scrollPos + height);

}

else if (!$curr)

$content.children(‘div:first-child’).addClass(o.selectClass);

}

 

function prevResult() {

$curr = getCurr();

 

if ($curr && $curr.prev().length > 0) {

$curr.removeClass(o.selectClass).prev().addClass(o.selectClass);

var scrollPos = $content.attr(‘scrollTop’),

curr = $curr[0],

parent = $curr.parent()[0],

parentTop, top, height;

if (supportsGetBoxObjectFor()) {

height = document.getBoxObjectFor(curr).height;

parentTop = document.getBoxObjectFor($content[0]).y – (height * 2); // TODO: this is not working when i add another control…

top = document.getBoxObjectFor(curr).y – document.getBoxObjectFor($content[0]).y;

}

else if (supportsGetBoundingClientRect()) {

parentTop = parent.getBoundingClientRect().top;

var rect = curr.getBoundingClientRect();

top = rect.top;

height = rect.bottom – top;

}

if (top <= parentTop)

$content.attr(‘scrollTop’, scrollPos – height);

}

else if (!$curr)

$content.children(‘div:last-child’).addClass(o.selectClass);

}

};

 

$.fn.flexbox = function (source, options) {

if (!source)

return;

 

try {

var defaults = $.fn.flexbox.defaults;

var o = $.extend({}, defaults, options);

 

for (var prop in o) {

if (defaults[prop] === undefined) throw (‘Invalid option specified: ‘ + prop + ‘\nPlease check your spelling and try again.’);

}

o.source = source;

 

if (options) {

o.paging = (options.paging || options.paging == null) ? $.extend({}, defaults.paging, options.paging) : false;

 

for (var prop in o.paging) {

if (defaults.paging[prop] === undefined) throw (‘Invalid option specified: ‘ + prop + ‘\nPlease check your spelling and try again.’);

}

 

if (options.displayValue && !options.hiddenValue) {

o.hiddenValue = options.displayValue;

}

}

 

this.each(function () {

new $.flexbox(this, o);

});

 

return this;

} catch (ex) {

if (typeof ex === ‘object’) alert(ex.message); else alert(ex);

}

};

 

// plugin defaults – added as a property on our plugin function so they can be set independently

$.fn.flexbox.defaults = {

source : [],

method: ‘GET’, // One of ‘GET’ or ‘POST’

queryDelay: 100, // num of milliseconds before query is run.

allowInput: true, // set to false to disallow the user from typing in queries

selectBehavior: true, // set to false to disallow the select behavior

containerClass: ‘ffb’,

contentClass: ‘content’,

selectClass: ‘ffb-sel’,

inputClass: ‘ffb-input’,

arrowClass: ‘ffb-arrow’,

matchClass: ‘ffb-match’,

noResultsText: ‘No matching results’, // text to show when no results match the query

noResultsClass: ‘ffb-no-results’, // class to apply to noResultsText

showResults: true, // whether to show results at all, or just typeahead

selectFirstMatch: true, // whether to highlight the first matching value

autoCompleteFirstMatch: false, // whether to complete the first matching value in the input box

highlightMatches: true, // whether all matches within the string should be highlighted with matchClass

highlightMatchesRegExModifier: ‘i’, // ‘i’ for case-insensitive, ‘g’ for global (all occurrences), or combine

matchAny: true, // for client-side filtering ONLY, match any occurrence of the search term in the result (e.g. “ar” would find “area” and “cart”)

minChars: 1, // the minimum number of characters the user must enter before a search is executed

showArrow: true, // set to false to simulate google suggest

arrowQuery: ”, // the query to run when the arrow is clicked

onSelect: false, // function to run when a result is selected

maxCacheBytes: 32768, // in bytes, 0 means caching is disabled

resultTemplate: ‘{name}’, // html template for each row (put json properties in curly braces)

displayValue: ‘name’, // json element whose value is displayed on select

hiddenValue: ‘id’, // json element whose value is submitted when form is submitted

initialValue: ”, // what should the value of the input field be when the form is loaded?

watermark: ”, // text that appears when flexbox is loaded, if no initialValue is specified.  style with css class ‘.ffb-input.watermark’

width: 200, // total width of flexbox.  auto-adjusts based on showArrow value

resultsProperty: ‘results’, // json property in response that references array of results

totalProperty: ‘total’, // json property in response that references the total results (for paging)

maxVisibleRows: 0, // default is 0, which means it is ignored.  use either this, or paging.pageSize

paging: {

style: ‘input’, // or ‘links’

cssClass: ‘paging’, // prefix with containerClass (e.g. .ffb .paging)

pageSize: 10, // acts as a threshold.  if <= pageSize results, paging doesn’t appear

maxPageLinks: 5, // used only if style is ‘links’

showSummary: true, // whether to show ‘displaying 1-10 of 200 results’ text

summaryClass: ‘summary’, // class for ‘displaying 1-10 of 200 results’, prefix with containerClass

summaryTemplate: ‘Displaying {start}-{end} of {total} results’ // can use {page} and {pages} as well

}

};

 

$.fn.setValue = function (val) {

var id = ‘#’ + this.attr(‘id’);

var input = $(id + ‘_input’);

input.val(val).removeClass(‘watermark’);

};

 

$.fn.setIdValue = function (val) {

var id = ‘#’ + this.attr(‘id’);

$(id + “_hidden”).val(val);

};

})(jQuery);

Publié par : yoannr | 17 juin 2011

Gestion des Exceptions, Unit Testing, MsTests

J‘aimerai avoir une fonction de ce type la pour tester mon code :

static void Throws<T>(Action action, string expectedMessage) where T : Exception

Malheureusement celle ci n’existe pas dans la collection Assert. J ai donc vu de part le net qu’il me fallait créer ma propre classe MyAssert avec cette méthode. Et tant qu’à faire je me suis dit que j’allais le faire en TDD.

Throws<T> va contenir des accès direct  à Assert, et par conséquent sera difficilement testable (encore que maintenant que j’ècris ce lignes je trouve que je me suis compliquée la vie, mais bon… Sourire )

Je me susi donc créer cette classe :

public class AssertAnswer
        {
            public bool Success { get; set; }
            public string Message { get; set; }
        }

et un fonction qui contiendrait le corps de ma logique :

public static AssertAnswer AssertAction<T>(Action action, string expectedMessage) where T : Exception
{
}

C’est cette fonction que je compte tester. J’écris donc mes tests :

private void ThrowNullReferenceException(string msg)
      {
          throw new NullReferenceException(msg);
      }
private void DoNotThrowException(string msg)
      {
          //do nothing
      }

[TestMethod]
        public void MyAssert_AssertAction_Exception_Awaited()
        {
            string msg = "test";
            var actual = MyAssert.AssertAction<System.NullReferenceException>(() => ThrowNullReferenceException(msg), msg);

            Assert.AreEqual(true, actual.Success);
            Assert.AreEqual(msg, actual.Message);
        }

[TestMethod]
        public void MyAssert_AssertAction_Different_Exception()
        {
            string msg = "test";
            var actual = MyAssert.AssertAction<System.ArgumentException>(() => ThrowNullReferenceException(msg), msg);

            Assert.AreEqual(false, actual.Success);
            Assert.AreEqual("A different Exception was thrown System.NullReferenceException.", actual.Message);
        }

[TestMethod]
        public void MyAssert_AssertAction_No_Exception()
        {
            string msg = "test";
            var actual = MyAssert.AssertAction<System.ArgumentException>(() => DoNotThrowException(msg), msg);

            Assert.AreEqual(false, actual.Success);
            Assert.AreEqual("Exception of type System.ArgumentException should be thrown.", actual.Message);
        }

Ces trois tests couvrent l’ensemble des cas de figure qui peuvent se passer.

Il ne reste plus qu’à écrire le code  de la fonction :

public static AssertAnswer AssertAction<T>(Action action, string expectedMessage) where T : Exception
       {
           AssertAnswer answer = new AssertAnswer();

           try
           {
               action.Invoke();

               answer.Success = false;
               answer.Message = string.Format("Exception of type {0} should be thrown.", typeof(T));
           }
           catch (T exc)
           {
               answer.Success = true;
               answer.Message = expectedMessage;
           }
           catch (Exception e)
           {
               answer.Success = false;
               answer.Message = string.Format("A different Exception was thrown {0}.", e.GetType());
           }

           return answer;
       }

Mes tests passent je peux continuer , et revenir a mon besoin initial , en réécrivant d’autres tests pour vraiment tester la fonction que je compte appeler plus tard :

[TestMethod]
       public void MyAssert_Throws_Exception_Awaited()
       {
           string msg = "test";
           MyAssert.Throws<System.NullReferenceException>(() => ThrowNullReferenceException(msg), msg);
       }

       [TestMethod]
       public void MyAssert_Throws_Different_Exception()
       {
           try
           {
               string msg = "test";
               MyAssert.Throws<System.ArgumentException>(() => ThrowNullReferenceException(msg), msg);

               Assert.Fail();
           }
           catch (Microsoft.VisualStudio.TestTools.UnitTesting.AssertFailedException e)
           {

           }
       }

       [TestMethod]
       public void MyAssert_Throws_No_Exception()
       {
           try
           {
               string msg = "test";
               MyAssert.Throws<System.ArgumentException>(() => DoNotThrowException(msg), msg);

               Assert.Fail();
           }
           catch (Microsoft.VisualStudio.TestTools.UnitTesting.AssertFailedException e)
           {

           }
       }

 

et ensuite le code le fonction pour réussir ces tests :

public static void Throws<T>(Action action, string expectedMessage) where T : Exception
       {
           AssertAnswer answer = AssertAction<T>(action, expectedMessage);

           Assert.IsTrue(answer.Success);
           Assert.AreEqual(expectedMessage, answer.Message);
       }

 

et voila, ma fonction est prête à être utilisé dans d’autres tests. Maintenant , il est vrai que j’aurais pu m’épargner la création de la fonction AssertAction et tester directement le code de ma fonction, mais que voulez vous, il faut bien débuter un jour et faire quelques erreurs.. d’ailleurs si vous en voyez d’autres n’hésitez pas..

++

Publié par : yoannr | 30 mars 2010

Jstree , Json et IEnumerable & Extension Method…

Bonjour,

Je suis toujours à ma découverte de JQuery, et dernièrement je me suis frotté á la problématique du treeview avec Jquery. Il n’y a pas encore de tree officiel dans Jquery et j’ai opté pour un des arbres qui est présent sur la road map de JQuery, j’ai nommé jstree (http://www.jstree.com/).

Ce tree permet de faire tout ce dont j’ai envie, créer des nodes insérer, effectuer des postback, travailler sur du HTML, du Json. Bref il me semble vraiment complet et assez convivial dans son ergonomie.

La documentation est assez bien faite et elle m’a amené a vouloir développer mon arbre en prenant comme source du Json et en mode asynchrone. Pour ce faire rien de plus simple il y a un exemple dans la doc  que j’ai remanié pour avoir cela:

<script type=”text/javascript” class=”source”>
$(function()
{
$(“#MyTree”).tree(
{
data:
{
type: “json”,
async: true,
opts:
{
url: “/localisation/GetTree”
}
}
});
});
</script>
<div id=”MyTree”>
</div>

l’arbre “myTree”  prend en donnée du Json, de maniére asynchrone et sa source de donnée sera un appel À l’URL “/localisation/GetTree”. Localisation est evidemment le nom de mon controlleur et GetTree le nom de l’action associée:

public string GetTree(int? id)
{

Localisation localisation = CreateLocalisation();

//fetch all groups
localisation.Groups = servicelocalisation.GetLocalisationGroups();

string returnvalue = string.Empty;
returnvalue = localisation.Groups.ToJsTreeJson(g=>g.ID.Value, g=>g.Name, g=> g.ChildGroups);

return returnvalue;
}

Le service localisation va récupérer nos donnée et nous rendre une List<LocalisationGroup>  stockée dans localisation.Groups .

J’ai ensuite développé une extension méthode afin de transformer mon IEnumerable<T> en Json afin que mon jsTree puisse interpréter correctement ma liste de groupe.

Si on détaille cette extension méthode, on voit trois choses entrée en argument :

  • g=>g.ID.Value
  • g=>g.Name
  • g=> g.ChildGroups

Ces trois expression lambdas vont nous aider a retrouver nos informations dans un LocalisationGroup. Elles vont servir respectivement a retrouver l’identifiant de notre groupe, le nom de notre groupe, et les groupes enfant de notre groupe.

Passons de l’autre coté du miroir et regardons de plus prés cette extension method:

public static string ToJsTreeJson<TElement>(
this IEnumerable<TElement> elements,
Func<TElement, int> GetIDElement,
Func<TElement, string> GetDataElement,
Func<TElement, IEnumerable<TElement>> GetChildrenElements)
{
string returnvalue = string.Empty;

JsTree tree = new JsTree();
foreach (TElement element in elements)
{
tree.RootNodes.Add(CreateJstreeNode(element,GetIDElement, GetDataElement,GetChildrenElements));
}

returnvalue = tree.Serialize();

return returnvalue;
}

Les choses se compliquent? pas tant que ca…  C’est une extension méthode donc la liste de groupe que j’avais au départ est en fait passé en argument et nous allons la retrouver sous le nom éléments. Et que vois t’on? Que pour chaque élément de cette liste je vais ajouter un treenode à notre tree. Normal c’est ce que l’on veut faire. Et pour ce faire on utilise la méthode CreateJstreeNode.

Cette méthode prend en argument presque la même chose que notre extension methode:

private static  JsTreeNode CreateJstreeNode<TElement>(
TElement element ,
Func<TElement, int> GetIDElement,
Func<TElement, string> GetDataElement,
Func<TElement, IEnumerable<TElement>> GetChildrenElements)
{
JsTreeNode node = new JsTreeNode();

node.data = GetDataElement(element);
node.attributes = new jsAttribute();
node.attributes.id =  GetIDElement(element).ToString();
foreach (TElement e in GetChildrenElements(element))
{
node.children.Add(CreateJstreeNode(e,GetIDElement, GetDataElement, GetChildrenElements));
}

return node;
}

Sauf que au lieu de s’attacher à voir ce qui se passe dans une liste d’éléments, cette dernière ne s’intéresse qu’au noeud qu’on lui donne en argument. Et grâce aux fonctions données précédemment (g=>g.ID.Value, g=>g.Name, g=> g.ChildGroups), elle va être capable de comprendre comment décortiquer notre objet afin d’instancier un JsTreeNode .
Pour finir, cette méthode s’appelle récursivement afin de détailler tous les éléments enfants qui pourraient appartenir à un élément.

Pour chaque élément de notre liste cette méthode va être appelée et ensuite notre objet JsTree sera rempli. Il ne suffria plus alors qu’à lui demander de se sérialiser afin de produire le json correspondant.

Afin d’être exhaustif voici le modèle utilisé pour représenter mon jstree :

public class JsTree
{
delegate string GetData();
public IList<JsTreeNode> RootNodes { get; set; }

public JsTree()
{
RootNodes = new List<JsTreeNode>();
}

public string Serialize()
{
string returnvalue = “[";
foreach (JsTreeNode node in RootNodes)
{
returnvalue += JSONSerializer.Serialize<JsTreeNode>(node)+",";
}
if (returnvalue.Length > 1)
{
returnvalue = returnvalue.Substring(0, returnvalue.Length - 1);
}
returnvalue += "]“;

return returnvalue;
}
}

public class jsAttribute
{
public string id { get; set; }
}

public class JsTreeNode
{
private List<JsTreeNode> _children;

public string data { get; set; }
public jsAttribute attributes { get; set; }
public List<JsTreeNode> children
{
get
{
if (_children == null)
_children = new List<JsTreeNode>();
return _children;
}
set
{
_children = value;
}
}
}

et voilà pour le Json Serializer :

public class JSONSerializer
{
public string unescapedUrl { get; set; }

public static T Deserialise<T>(string json)
{
T obj = Activator.CreateInstance<T>();
using (MemoryStream ms = new MemoryStream(Encoding.Unicode.GetBytes(json)))
{
DataContractJsonSerializer serializer = new DataContractJsonSerializer(obj.GetType());
obj = (T)serializer.ReadObject(ms);
return obj;
}
}

public static string Serialize<T>(T obj)
{
DataContractJsonSerializer serializer = new DataContractJsonSerializer(obj.GetType());
using (MemoryStream ms = new MemoryStream())
{
serializer.WriteObject(ms, obj);
return Encoding.Default.GetString(ms.ToArray());
}
}
}

Comme toujours, si vous avez des questions ou des commentaires sur ma manière de procéder , ne vous gêner pas, ce blog est un peu fait pour ca!

a++

Cet petit article fait suite à celui ci où j’expliquais ma façon de gérer le stockage des données dans mon application. Les exemples choisis étaient volontairement simple afin de mieux me concentrer sur l’architecture que je proposais. Aujourd’hui, je vais continuer en proposant un nouveau type de container pour faire persister les objets.

Je travaille actuellement sur un site web hébergé en Web Farm. Il doit y avoir un dizaine de serveur je pense dans ma ferme, et je me heurte par conséquent À un problème, celui des variables d’application.

En effet si je modifie dynamiquement ma variable d’application via un interface web, je ne l’aurais fait en fait que sur un seul serveur pas sur les 9 autres, qui continueront avec leurs propres collection de variable d’application. Pour mettre à jour, le s9 autres , il va falloir que que je rappelle ma site plusieurs fois afin d’avoir la chance de tomber sur tous mes serveurs de ma ferme et ainsi mettre a jour ma valeur. Bref pas très pratique tout ca..

Pour cela j’ai imaginé un nouveau type de container. Rien de bien sorcier en fait, il s’agit juste d’un cache avec une durée d’expiration et qui prend en entrée une fonction. Cette fonction lui indique comme aller chercher les données à mettre à jour.

Ainsi, si je mets a jour une variable d’application, je met a jour en même temps ma base de données, et au bout de x minutes, chaque container ira de lui même , grâce à la fonction, rechercher la valeur qui va bien pour chaque serveur de ma web farm.

Un peu de code pour illustrer mon propos. Voici une classe MonApplication qui contient les données générales propres au contexte de mon appli.

public class MonApplication
{
public DateTime? AppOfflineStartDate { get; set; }
public DateTime? AppOfflineEndDate{ get; set; }
public bool IsAppOffline{ get; set; }
public Robots Robot{ get; set; }

public bool GetApplicationOnline()
{
bool returnvalue = this.IsAppOffline;

//the dates indicated have priority on the status to put the application  offline
if (this.AppOfflineStartDate.HasValue && this.AppOfflineEndDate.HasValue)
{
if (DateTime.Now > this.AppOfflineStartDate.Value && DateTime.Now < this.AppOfflineEndDate)
returnvalue = true;
}
else
{
// either AppOfflineStartDate or AppOfflineEndDate is null, may be both of them

if (this.AppOfflineStartDate.HasValue)
{
if (DateTime.Now > this.AppOfflineStartDate.Value)
returnvalue = true;
}

if (this.AppOfflineEndDate.HasValue)
{
if (DateTime.Now < this.AppOfflineEndDate)
returnvalue = true;
}

if (!this.AppOfflineStartDate.HasValue && !this.AppOfflineEndDate.HasValue)
{
//do nothing the basic IsAppOnline should prevail
}
}

return returnvalue;
}

}

Rien de Bien compliqué ici. Il s’agit juste de savoir si mon application doit être on line ou non. Et pour cela, on va chercher les informations dans un base de données grâce à ce service.

public class KjuApplicationService
{
public KjuApplication GetKjuApplication()
{

KjuApplication kjuappli = new KjuApplication();

//Bloc de code pour aller chercher les données dans la base de données ici

Robots Bob = new Robots();
Bob.Indexed = true;
Bob.LinkTofollows = true;

kjuappli.Robot = Bob;

return kjuappli;
}
}

L’appel À notre container va se faire de la sorte :

public static MonApplication monAppli {
get {
StoringContainerProvider container = StoringContainerFactory.GetInstance(StoringContainerFactory.E_StoringContainerType.DynamiqCache);
KjuApplicationService kas =new KjuApplicationService();
return (KjuApplication)((StoringContainerDynamiqCache)container).LoadObject(“KjuAppli”, kas.GetKjuApplication, 5);
}
set {
StoringContainerProvider container = StoringContainerFactory.GetInstance(StoringContainerFactory.E_StoringContainerType.DynamiqCache);
container.SaveObject(value,”KjuAppli”);
}
}

Ce qui change ici est exclusivement la partie get, car avec mon nouveau container “DynamiqCache”, je vais lui donner la fonction du service que j’ai détaillée avant.

public object LoadObject(string ContainerKey, Func<Object> FetchObject, double minutes)
{
//have a look if the cached object is not present
if (HttpContext.Current.Cache.Get(ContainerKey) == null)
{
Object ObjToBeSaved = FetchObject();
this.SaveObject(ObjToBeSaved, ContainerKey, minutes);
}
return HttpContext.Current.Cache[ContainerKey];
}

Et voilà le travail, j’ai maintenant un container générique qui va pouvoir mettre À jour ses données si on lui dit comment faire.

N’hésitez pas à me laisser des commentaires si vous voyez des erreurs ou si si vous trouvez qu’il y a des choses À améliorer.

Bon je continue mon petit bonhomme de chemin. Aujourd’hui je vais tacher de rendre ma liste de groupe un peu plus sexy en lui rajoutant des informations intéressantes.

  1. le nom du groupe dans lequel je me trouve
  2. le chemin du groupe dans lequel je me trouve

pour le point 1 pas de gros souci, mais cela va me permettre de détailler un peu le fonctionnement d’accès aux données que j’ai changer depuis mon dernier billet. Maintenant mon contrôleur n’accède plus directement au repository mais plutôt à une couche de service. cela me permet d’écrire le code suivant dans mon contrôleur.

// GET: /Localisation/ListGroup/id
public ActionResult ListGroup(int? ParentGroupId)
{
Localisation localisation = CreateLocalisation();
localisation.Groups = servicelocalisation.GetLocalisationGroups(ParentGroupId);
if (ParentGroupId.HasValue)
{
localisation.CurrentGroup = servicelocalisation.GetLocalisationGroup(ParentGroupId.Value);
localisation.GroupParents = servicelocalisation.GetLocalisationGroupParents(ParentGroupId.Value);
}
return View(“ListGroup”, localisation);
}

localisation.CurrentGroup va contenir le groupe parent de ma liste, celui dont j’explore les groupes enfants. Et pour le récupérer je vais faire un appel à la fonction GetLocalisationGroup du service servicelocalisation. Cette fonction renvoie un localisation group et interroge le repository :

public LocalisationGroup GetLocalisationGroup(int LocalisationGroupId)
{
LocalisationGroup group = _repository.FindLocalisationGroup(LocalisationGroupId);
return group;
}

Au vu de cet exemple on pourrait se poser la question du bien fondé d’avoir une couche service, mais celle ci prend tout son sens pour par exemple pour les fonctions GetLocalisationGroups() et GetLocalisationGroups(int? GroupParentId) qui servent respectivement à récupérer tous les groupes et à récupérer tous les groupes possédant un certain groupe parent. On verrait alors deux traitements différents d’un appel à la même fonction de notre repository FindAllLocalisationGroup.

//get all the groups in the database
public IList<LocalisationGroup> GetLocalisationGroups()
{
IQueryable<LocalisationGroup> groups = _repository.FindAllLocalisationGroup();
}

et

//get the groups whom parent id is given
public IList<LocalisationGroup> GetLocalisationGroups(int? GroupParentId)
{
IQueryable<LocalisationGroup> groups = _repository.FindAllLocalisationGroup().Where(x => (x.ParentGroup.Id == GroupParentId) || (x.ParentGroup==null && !GroupParentId.HasValue));
return groups.ToList();
}

L’intérêt ici est que ce qui sort de notre repository n’est qu’un IQueryable, ce qui signifie que l’appel à la base de données n’a pas été fait. Il ne sera fait que dans notre couche service, et donc la requête SQL sera appropriée a la demande initiale. Il n’y aura pas de requête du style : “ Select * from ma table”, pour ensuite faire un filtrage sur les résultats, ce qui serait un gâchis énorme.

Trêve de digression, je reviens à mon sujet initial. Et j’aborde le point 2. Beaucoup plus intéressant. Je veux connaitre pour un groupe donné tous les parents  de celui ci. Pour ce faire, j’ai de la chance j’ai une procédure stockée qui me renvoie l’information sous forme de liste. Parfait, il n’y a plus qu’à extraire ces informations de ma procédure stockée!

La première chose à faire est d’écrire ma proc. stock dans ma base de données. Elle a pour nom [SP_LocalisationGroup_GetParents] et prend en argument un entier nullable : @localisationgroup_childid int=null

Maintenant tâche à nous de faire comprendre à EF ce que nous voulons. Je me place donc dans mon modèle de données.

ef2-1

et je fais un clic droit pour actualiser mon modèle depuis ma base de données.

je choisis ma procédure stockée à ajouter.

ef2-2

Et je valide.

Ensuite je me place dans le model Browser et dans localisation.Store, je vois ma procédure stockée qui a été rajouté. Afin d’avoir un accès facile à cette dernière, je clic droit sur elle et je choisis add function import.

ef2-3

Cela va me rajouter une fonction d’import dans mon conteneur et ainsi me permettre de prendre en main l’appel à cette procédure stockée de cette façon :

public IList<LocalisationGroup> GetLocalisationGroupParents(int groupid)
{
IList<LocalisationGroup> parentgroups = container.SP_LocalisationGroup_GetParents(groupid).ToList();
return parentgroups;
}

Vous remarquerez ici que l’accès à la base de données est immédiat. cela signifie donc que lorsque j’appellerais cette fonction, je ferais un appel à ma base, en renvoyant un IQueryable ce n’est pas le cas. Mais ici ce n’est pas important, car je récupère directement ce qui m’intéresse sans avoir à faire un traitement particulier dessus.

Voilà, je pense avoir rajouter une petite brique de plus. Je vais continuer à travailler sur ce petit projet au rythme de mon temps disponible… donc ne vous étonnez pas d’avoir des nouvelles dans 48 heures ou dans 3 mois!

Merci de votre lecture, et comme d’habitude si vous avez des suggestions, des commentaires, ou autres, surtout n’hésitez pas.

Bon ca fait un petit moment que je devais revenir. J’avais prévu une semaine, au final, cela aura fait deux mois. Entre temps beaucoup de tafs et d#autres choses mais on est pas la pour parler de ma vie, enfin pas que.. ;)

Je me suis beaucoup intéresse au développement sous AJAX ces derniers temps, et surtout ce qui est faisable avec MVC ASP.net. Le résultat n’est pas encore probant, je n’ai pas encore réussi a faire vraiment ce que je voulais, mais je vais vous livrer une première ébauche de ce vers quoi je tends, avec un peu de chance, j’aurais peut être même deux ou trois commentaires qui me guideront vers la sortie…

L’objectif est pour le moment de faire apparaitre  un tableau avec les valeurs de ma table de LocalisationGroup et de pouvoir une popup pour éditer un élément de ma table.

Pour cela j’ai le code suivant :

<h2>Groups</h2>
<div id=”GroupList”></div>
<div id=”GroupDetail” title=”Detail Group”>
</div>

Comme vous pouvez le voir ridiculeusement simple. Ma div grouplist sera peuplé par un appel javascript :

function RefreshListGroup(id) {
//load the list of group
$(“#GroupList”).load(“/Localisation/ListGroup”, { ParentGroupId: id });
}

//Intialisation of the page
RefreshListGroup(null);

Cet appel JavaScript va charger le contenu Html qui sera renvoyé lors de l’appel à l’action ListGroup du contrôleur Localisation, et elle prend en paramètre le parent group id car je le rappelle, ma liste de données possède une structure hiérarchique.

tableau

De quoi est composé cette action :

// GET: /Localisation/ListGroup/id
public ActionResult ListGroup(int? ParentGroupId)
{
Localisation localisation = CreateLocalisation();
localisation.Groups = servicelocalisation.GetLocalisationGroups(ParentGroupId);
localisation.AllGroups = servicelocalisation.GetLocalisationGroups();
return View(“ListGroup”, localisation);
}

Elle renvoie une vue nommé List group qui correspond à un usercontrol qui contient mon tableau. J’aurais pu associer a ce usercontrol une banale liste de localisationgroup, mais je vais avoir besoin de plus d’information ensuite, donc j’ai un DTO qui m’assure le transport de toutes mes informations vers ma vue.

Ce contrôle qui contient mon code Html est le suivant:

<%@ Control Language=”C#” Inherits=”System.Web.Mvc.ViewUserControl<Localisation>” %>

<table class=”list” width=”100%” cellspacing=”0″ border=”1″ rules=”all”>
<tr class=”headerStyle”>
<td>
&nbsp;
</td>
<td>
<input type=”checkbox” enabled=”false” />
</td>
<td>
<asp:Image ID=”Image2″ runat=”server” ImageUrl=”~/App_Themes/dynamiq/true.gif” />
</td>
<td width=”100%”>
Name
</td>
<td >
&nbsp;
</td>
</tr>
<% foreach (Data.LocalisationGroup group in Model.Groups )
{%>
<tr class=”itemStyle”>
<td>
<%
if (Model.CurrentGroup.Id>0)
{
Response.Write(Html.Image(this.ResolveUrl(“~/App_Themes/dynamiq/attention.gif”),
“The dates of this group are not in the boundaries of its parent”,
!group.MatchParentKeyDates(Model.CurrentGroup.StartDate, Model.CurrentGroup.EndDate)));
}
%>
</td>
<td>
&nbsp;
</td>
<td>
<%= group.IsReleased %>
</td>
<td >
<a href=”Javascript:RefreshListGroup(<%= group.Id %>);”><%= group.Name %></a>
</td>
<td >
<%= Ajax.ActionLink(“Edit”, “DetailLocalisationGroup”, new { id = group.Id }, new AjaxOptions() { UpdateTargetId = “DetailLocalisationGroup”, OnSuccess = “InitialisationDetailGroup” })%>
</td>
</tr>
<%  }%>
</table>

Rien de bien compliqué ici, je me contente d’afficher les informations dans différentes lignes. La seule dynamicité est là :

<a href=”Javascript:RefreshListGroup(<%= group.Id %>);”><%= group.Name %></a>

Cet appel me permet de recharger ma list de localisationGroup, au cas ou je désire naviguer dans ma liste hiérarchique.

et le deuxième point est là :

<%= Ajax.ActionLink(“Edit”, “DetailLocalisationGroup”, new { id = group.Id }, new AjaxOptions() { UpdateTargetId = “DetailLocalisationGroup”, OnSuccess = “InitialisationDetailGroup” })%>

Ici j’utilise une propriété du framework MVC . Il s’agit là d’appeler l’action DetailLocalisationGroup du contrôleur Localisation, et de rendre l’html produit dans la div DetailLocalisationGroup.

Ainsi en cliquant sur le lien  , le contenu du div groupdetail contiendra le formulaire de modification de mon objet localisationgroup

<h2>Groups</h2>
<div id=”GroupList”></div>
<div id=”GroupDetail” title=”Detail Group”>
</div>

l’action du controleur Localisation est la suivante :

[AcceptVerbs("POST")]
public ActionResult DetailLocalisationGroup(int id)
{
LocalisationGroup group = servicelocalisation.GetLocalisationGroup(id);
return View(“DetailGroup”, group);
}

Elle renvoie un usercontrol nommé DetailGroup, avec le group demandé :

<%@ Control Language=”C#” Inherits=”System.Web.Mvc.ViewUserControl<Data.LocalisationGroup>” %>
<form id=”EditLocalisationGroupForm” >
<p>
Id:
<%= Html.Encode(Model.Id)%>
<%= Html.TextBox(“Id”, Html.Encode(Model.Id), new { @readonly = “true” })%>
</p>
<p>
Name:
<%= Html.TextBox(“Name”, Html.Encode(Model.Name))%>
<%= Html.ValidationMessage(“Name”, “*”)%>
</p>
<p>
ParentGroupId:  <%= Html.TextBox(“ParentGroupId”, Html.Encode(Model.ParentGroupId))%>
</p>
</form>

A ce stade, je me retrouverait donc avec mes deux div de départ chargés du Html qui va bien. Et pour mettre un peu de bonhommie dans tout ca, j’aurais pris soin d’utiliser un peu de Jquery pour mettre en forme mon formulaire d’edition, et ce JQuery provoquera aussi la soumission de mes informations :

$(function () {

$(“#DetailLocalisationGroup”).dialog({
bgiframe: true,
autoOpen: false,
height: 500,
width: 300,
modal: true,
buttons: {
‘Save’: function () {
var bValid = true;

//Validation du contenu : bValid=true or false

if (bValid) {
dataString = $(“#EditLocalisationGroupForm”).serialize();

$.ajax(
{
type: “POST”,
url: “/Localisation/EditLocalisationGroup”,
data: dataString,
dataType: “html”,
success: function (data) {
$(“#GroupList”).html(data);
},
error: function (httpRequest, textStatus, errorThrown) {
alert(“status=” + textStatus + “,error=” + errorThrown);
}
}
);

$(this).dialog(‘close’);
}
},
Cancel: function () {
$(this).dialog(‘close’);
}
},
close: function () {
allFields.val(”).removeClass(‘ui-state-error’);
}
});

$(‘#create-user’).click(function () {
$(‘#DetailLocalisationGroup’).dialog(‘open’);
})
$(‘#edit-group’).click(function () {
$(‘#DetailLocalisationGroup’).dialog(‘open’);
});

});

Edit

J’utilise ceci pour envoyer mes données à l’action EditLocalisationGroup du controleur Localisation :

dataString = $(“#EditLocalisationGroupForm”).serialize();

$.ajax(
{
type: “POST”,
url: “/Localisation/EditLocalisationGroup”,
data: dataString,
dataType: “html”,
success: function (data) {
$(“#GroupList”).html(data);
},
error: function (httpRequest, textStatus, errorThrown) {
alert(“status=” + textStatus + “,error=” + errorThrown);
}
}
);

Le contrôleur récupère un groupe qu’il tachera de sauvegarder dans la base de données :

[AcceptVerbs("POST")]
public ActionResult EditLocalisationGroup(LocalisationGroup group)
{
servicelocalisation.SaveLocalisationGroup(group);
return ListGroup(group.ParentGroupId);
}

En cas de succès, je récupère le contenu de mon tableau que je rempalce dans ma div. je referme ensuitema popup:

$(this).dialog(‘close’);

Voilà pour l’instant , où j’en suis rendu. Les choses avancent tout doucement. Il y a plusieurs choses que je n’aime pas trop comme le fait de faire appel a des postback via Jquery . J’aurais préféré quelque chose de plus intégré dans le Framework asp.net MVC. Mais peut être m’y suis je mal pris, je compte continuer un peu mes investigations dans ce domaine.

Si vous avez des idées, des commentaires ou des remarques a faire sur la façon que j’ai de procéder n’hésiter surtout pas, je suis dans une phase d’exploration en ce moment…

le projet est téléchargeable en l’état, c’est à dire en construction ici.

merci de votre lecture,

Avant de se lancer dans des développements un peu trop compliqué, je voudrais savoir un peu comment marche la chose, au moins dans ses grandes lignes, et comment je vais pouvoir en tirer parti.

Entity Framework

 

Le modele edmx ( il va falloir que je me renseigne sur les termes avant la fin, moi…) me génère mes objets tout seul. ca j’apprécie beaucoup. je me retrouve donc avec un objet comme suit ( en fait c’est beaucoup plus long, mais j’ai décidé de me retreindre à l’essentiel) :

[EdmEntityTypeAttribute(NamespaceName="Localisation", Name="Culture")]
[Serializable()]
[DataContractAttribute(IsReference=true)]
public partial class Culture : EntityObject
{
    public global::System.Int32 Id {get; set;}
    public global::System.String Language {get; set;}
    public global::System.String Country {get; set;}
}

Pour l’instant, je ne regarderais pas plus avant ce qu’ il y a dedans.

Et je ne m’attarderais pas non plus sur toutes les fonctions possibles contenues le modele edmx, mais par contre je vais les utiliser dans un classe RepositoryCulture.

public class CultureRepository: ICultureRepository
   {

       LocalisationContainer container ;

       public CultureRepository(string connectionstring)
       {
           container = new LocalisationContainer(connectionstring);
       }

       // Query Methods
       public IQueryable<Culture> FindAllCulture()
       {
           return  container.T_Culture;
       }
       public Culture FindCulture(int CultureId)
       {
           var culture=  from c in FindAllCulture()
                          where c.Id == CultureId
                           select c;
           return culture.SingleOrDefault();
       }

       // Insert/Delete
       public void AddCulture(Culture c)
       {
           container.T_Culture.AddObject(c);
           container.SaveChanges();
       }
       public void DeleteCulture(Culture c)
       {
           container.T_Culture.DeleteObject(c);
           container.SaveChanges();
       }

       // Persistence
       public void Save()
       {
           container.SaveChanges();
       }
   }

Cette dernière va me permettre de manipuler mon edmx, de façon sereine. Mais cela veut donc dire que je vais utiliser une interface afin de me préserver d’un éventuel changement d’humeur de ma part ( je pourrais avoir envie d’essayer Nhibernate par exemple, plus tard…  grrr…)

public interface ICultureRepository
    {
    // Query Methods
    IQueryable<Culture> FindAllCulture();
    Culture FindCulture(int CultureId);

    // Insert/Delete
    void AddCulture(Culture c);
    void DeleteCulture(Culture c);
    // Persistence
    void Save();

    }

 

Je dispose donc maintenant d’une méthode conviviale pour jouer avec mes données de Culture. Passons au coté MVC…

MVC

l’acces aux donnés

De la même façon, que je suis parti sur un concept simple pour le coté Entity Framework, je ferais de même pour ma première approche de MVC.

La première chose que je veux faire, c’est de pouvoir accéder aux donnés facilement dans mon application MVC, je vais donc poursuivre le travail effectué plus avant, et généraliser ma façon d’accéder aux infos. Pour ce faire, je vais utilser un composant d’injection de dépendance / Inversion de Contrôle  nommé StructureMap

Je ne susi réellement pas un expert de ce genre de composé, j ai donc repiqué beaucoup d’idée a un tutorial MVC de microsoft. Mais voilà ce que j’ai pu en tirer pour mon cas présent.

dans mon application_start , j’ai rajouté

        BootStrapper.ConfigureStructureMap();

           ControllerBuilder.Current.SetControllerFactory(
               new StructureMapControllerFactory()
               );

Cela me permet d’initialiser le composant. la classe BootStrapper consiste en deux lignes :

public class BootStrapper
   {
       public static void ConfigureStructureMap()
       {
           StructureMapConfiguration.AddRegistry(new DBServiceRegistry()); 
           StructureMapConfiguration.AddRegistry(new StorefrontRegistry());
       }
   }

Et la classe DBServiceReigistry, va me permettre d’instancier ma conenction à la bonne base pour entity :

public class DBServiceRegistry : Registry
    {
        public string ConnectionString
        {
            get
            {
                if (WebConfigurationManager.ConnectionStrings["Application"]==null)
                    throw new InvalidOperationException(“Conenction string Application cannot be null.”);
                if (string.IsNullOrEmpty(WebConfigurationManager.ConnectionStrings["Application"].ToString()))
                    throw new InvalidOperationException(“Connection string Application cannot be empty.”);

                return WebConfigurationManager.ConnectionStrings["Application"].ToString();
            }
        }

        protected override void configure()
        {

            ForRequestedType<System.Data.EntityClient.EntityConnection>()
                .TheDefaultIs(() => new System.Data.EntityClient.EntityConnection(ConnectionString))
                .CacheBy(InstanceScope.Hybrid);

        }
    }

tandis que StoreFrontRegistryx lui cataloguera, les différents repository que je peux avoir et instanciera la bonne classe.

public class StorefrontRegistry : Registry
    {
        protected override void configure()
        {

            #region repository
            ForRequestedType<ICultureRepository>()
              .TheDefaultIsConcreteType<CultureRepository>();

            ForRequestedType<ILocalisationRepository>()
              .TheDefaultIsConcreteType<LocalisationRepository>();
            #endregion
        }
    }

Pour l’instant, j’utilise une autre classe StructureMapControllerFactory, mais je n’ai pas encore pris le temps de changer quelque chose à l’intérieur, le besoin ne s’en est pas fait sentir tout du moins.

les routes

Pour mes routes je ne me suis pas trop embêté pour l’instant, et j ‘ai tapé dans le simple et efficace. J’ai codé en dur Localisation/Culture afin de faire un différence significative avec ce qui pourrait arrivé ensuite :

 

public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute(“{resource}.axd/{*pathInfo}”);

            routes.MapRoute(
                “home”,                                                  // Route name
                “home/{controller}/{action}/{id}”,                       // URL with parameters
                new { controller = “Home”, action = “Index”, id = “” }   // Parameter defaults
            );

            routes.MapRoute(
               “localisation”,                                              // Route name
               “localisation/culture/{action}/{id}”,                        // URL with parameters
                new { controller = “Culture”, action = “Index”, id = “” }   // Parameter defaults
           );

        }

Par ailleurs avant d’aller plus loin , j ai installé un autre composant RouteDebug.dll qui me permet de debugger mes routes, je pense que tôt ou tard, j’en aurais besoin.

Pour ce faire, j’ai rajouter mais commenté pour le moment l’instruction suivante :

          // ROUTES
           RegisterRoutes(RouteTable.Routes);
           //uncomment this line for route debugging
           //RouteDebug.RouteDebugger.RewriteRoutesForTesting(RouteTable.Routes);

On peut voir dans ma route que j’instancie :

       new { controller = “Culture”, action = “Index”, id = “” } 

Ce faisant, j’instancie un contrôle nommé Culture.

le controller

Rien de plus simple que ce controller. Mais tout d’abord, faire attention à son vrai nom CultureController. C’est important afin que MVC ne se mélange pas les pinceaux.

Ensuite détaillons le controller :

son constructeur :

#region constructor

       public CultureController(ICultureRepository repository)
       {
           _repository = repository;
           if (_repository == null)
               throw new Exception(“Repository cannot be null”);
       }

       #endregion

Il prend en entrée un ICultureRepository  qui sera instancié par StructureMap avec la bonne classe.

et ensuite correspondant à l’ {action} de ma route :

      // GET: /Localisation/Culture/Index ou /Localisation/Culture
        public ActionResult Index()
        {
            IQueryable<Culture> cultures = _repository.FindAllCulture();
            return View(“Index”, cultures);
        }

Suivant l’url /Localisation/Culture/Index ou /Localisation/Culture, j’appellerais le Controller Culture et j’effectuerais l’action Index, qui me renverra la vue Index.

La vue Index prend en parametre un IQueryable<Cultures>, que je vais chercher au moment ou j’en ai besoin.

Cela fonctionne de la même facon pour les autres actions, avec un petit détail à voir, à savoir la méthode utilisé pour appeler l’url qui permettra sur la même Url De procéder à plusieurs actions différentes :

public ActionResult Details(int id)
public ActionResult Create()

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(FormCollection collection)

[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Edit(int id)

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection collection)

[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Delete(int id)

Sur ce , il n’ y a plus qu’à créer les vues correspondantes

les Views

Personnellement, et pour le début, je ne me suis pas embêté, et j’ai pris l’assistant de visual studio pour les créer.  J’ai donc créé un répertoire Culture dans Views. Ceci est aussi important pour MVC.

Je pense qu il doit y avoir un moyen de changer ce paramètre par défaut , mais ce sera l’occasion de plus amples investigations.

Voilà ,c’est tout pour aujourd’hui. Bon c’est bien évidemment hyper débutant comme approche, mais c’Est ce que je suis, et je me disais que d’autres pourraient être intéressé par mon cheminement personnel, si jamais il y avait des amateurs de MVC et d’entity framework dans les parages!

La suite la semaine prochaine,

L’objectif que je me fixe dans les jours qui viennent est de réaliser une petite application de localisation en ASP.net et MVC. Il s’agit pour moi surtout de me familiariser avec ces techniques, et peut être que je pourrais vous emmener avec moi sur un petit bout de chemin, qui sait?

Tout d’abord le but, je vais m’exprimer avec les mots que je connais a savoir ceux de l’univers webform: Avoir une librairie de contrôle qui accepteront un clé (localisationKey) et un langage. Cette clé possède une date de départ et une date de fin. Et associé à cette clé, je veux avoir un article (LocalisationItem) qui lui contiendra le texte a afficher en fonction du langage. Cela permettra de pouvoir aficher un texte en plusieurs langues et qui pourra changer aussi en fonction de la date.

Bref, rien de bien compliqué, surtout que j’ai déjà fait tout ca en webform. Il s’agit aussi pour moi, de voir l’effort à fournir pour transoposer vers le support MVC.net du travail déjà fait en webform.

Dernière petite précision, l’intérêt va être aussi d’utiliser Entity framework de façon à générer ma base de données à partir du modèle.

Les outils à ma disposition :

  • Visual Studio 2010
  • Visual Studio 2005 expresse édition

Tout d’abord mon premier schéma de données :

MVC1

Comme vous pouvez le constater, je pars du principe que je vais avoir des groupes qui contiendront des  groupes enfants, ou et des clés.

Et à chacune de ces clés, j’associe un item.

Lors de la création de mon schéma j’ai pris soin de spécifier pour chacun des id , la propriété StoreGeneratedPAttern=Identity. Cela me permet d’avoir un champ auto incrémenté du coté de ma base de données.

Du coté de ma solution, je m’oriente vers différents projets :

  • site MVC
  • Data
  • Services
  • Test

Tout ce ci n’est évidemment qu’une première approche, au fur et á mesure de l’avancée du projet j’affinerais un peu tout ca.

Voici le projet utilisé actuellement : solution VS 2010

Suite dans l’étape 2…

Bonjour,

Dans les applications ASP.net, on doit comme dans la plupart des application stocker  nos données lors de l’exécution du programme. En ASP.net, cela peut ête dans des variables d’application, de session, dans le cache, dans le viewstate. Mais on peut imaginer aussi un conteneur exprès pour accueillir des objets un certain laps de temps, ou d’autres choses encore plus fantaisistes mais pourtant tellement quotidienne…

Par ailleurs, dans les applications asp.net, on peut etre confronté aussi à un autre problème: Comment arriver à obtenir de la souplesse avec le mode d’hébergement? je m’explique par un exemple, comment faire dans un site de e-commerce pour passer d’un caddie hébergé en variable de session à un caddie hébergé dans le viewstate ou dans le cache, etc..

Dans mes applications, j’utilise une seule classe avec des méthodes static . Et des lors que je veux quelque chose entreposé dans un container c’est à cette classe que je m’adresse. Un petit exemple avant de continuer :

/// <summary>

/// this class defines where to get all the items used to make the web site work

/// This is where to define the storage of every item

/// </summary>

public class MonContext

{

/// <summary>

/// Stored in the session

/// </summary>

/// <returns></returns>

public static UserProvider GetCurrentUser()

{

StoringContainerProvider container = StoringContainerFactory.GetInstance(_
StoringContainerFactory.E_StoringContainerType.Session);

if (container.LoadObject(“User”) == null)

{

UserProvider user = new UserProvider();

container.SaveObject(user, “user”);

}

return (UserProvider)container.LoadObject(“User”);

}

}

Cette méthode GetCurrentUser() va être responsable du container pour entreposer ma variable. Ici nous voyons un container de type simple : une session. Rien de bien excitant.
Mais de la même facon, je pourrais appeler un autre type de conteneur. Maintenant , un petit tour sur la fabrication d’un conteneur. Celui ci va devoir implémenter l’interface suivante :

public interface IContainer {

string GetContainerKey(params Object[] listparams);

List<string> GetKeys();

bool HasObject(string ContainerKey);

object LoadObject(string ContainerKey);

void SaveObject(Object objtoBeSaved, string ContainerKey);

void DeleteObject(string ContainerKey);

}

Et j’aurais alors une factory qui me permettra de choisir le type du conteneur qui m’interesse :

public sealed class StoringContainerFactory

{

/// <summary>

/// Type of container available

/// </summary>

public enum E_StoringContainerType{

none,

Session,

Application,

RawCache,

MonWebContainer

}

private E_StoringContainerType _StoringContainerType;

public E_StoringContainerType StoringContainerType{

get{ return _StoringContainerType; }

set{_StoringContainerType = value; }

}

public static StoringContainerProvider GetInstance(_

E_StoringContainerType containertype){

StoringContainerProvider Instance = null;

switch (containertype)

{

case E_StoringContainerType.none:

Instance = new ConcreteProvider.StoringContainerNone();

break;

case E_StoringContainerType.Session:

Instance = new ConcreteProvider.StoringContainerSession();

break;

case E_StoringContainerType.Application:

Instance = new ConcreteProvider.StoringContainerApplication();

break;

case E_StoringContainerType.RawCache:

Instance = new ConcreteProvider.StoringContainerRawCache();

break;

case E_StoringContainerType.MonWebContainer:

Instance = new ConcreteProvider.StoringContainerMonWebContainer();

break;

}

return Instance;

}

}

J’utilise un énuméré E_StoringContainerType dans ma factory car je trouve cela plus tranquille de connaitre les possibilités que j’ai d’entreposer des choses.

Cette factory nous renvoie des objets abstraits  de type StoringContainerProvider :

public abstract class StoringContainerProvider : IContainer,IDisposable{

public string GetContainerKey(params Object[] listparams){

string returnvalue=string.Empty;

for (int i = 0; i < listparams.GetLength(0); i++)

{

if (listparams[i] != null)

{

returnvalue += listparams[i].ToString();

}

else

{

returnvalue += “null”;

}

}

return returnvalue;

}

public abstract void DeleteObject(string ContainerKey);

public abstract List<string> GetKeys();

public abstract bool HasObject(string ContainerKey);

public abstract Object LoadObject(string ContainerKey);

public abstract void SaveObject(Object ObjToBeSaved, string ContainerKey);

}

Ces objets abstraits ne sont rien tant qu’ils ne sont pas instanciés avec un objet plus concret. Prenons l’exemple de rawCache:

public class StoringContainerRawCache : StoringContainerProvider{

public override bool HasObject(string ContainerKey)

{

return (HttpContext.Current.Cache[ContainerKey.ToLower()] != null);

}

public override object LoadObject(string ContainerKey)

{

return HttpContext.Current.Cache[ContainerKey.ToLower()];

}

public override void SaveObject(Object ObjToBeSaved, string ContainerKey)

{

if (ObjToBeSaved == null)

{

this.DeleteObject(ContainerKey.ToLower());

}

else

{

HttpContext.Current.Cache[ContainerKey.ToLower()] = ObjToBeSaved;

}

}

public override void DeleteObject(string ContainerKey)

{

HttpContext.Current.Cache.Remove(ContainerKey.ToLower());

}

public override List<string> GetKeys()

{

List<string> ls = new List<string>();

IDictionaryEnumerator d = HttpContext.Current.Cache.GetEnumerator();

d.MoveNext();

for (int i = 0; i < HttpContext.Current.Cache.Count;i++ )

{

if (d.Current != null)

{

ls.Add(d.Key.ToString());

d.MoveNext();

}

}

return ls;

}

}

Ici la gestion est aussi très simple, mais on pourrait imaginer , une gestion plus complexe qui aurait lieu dans cette classe. Ensuite Tout dépend des besoins d l’application à vrai dire. J’espère  vous avoir introduit à un système de gestion de vos conteneurs dans vos applications.

C’est agréable de travailler de la sorte ensuite, car vous ne vous poser plus aucune question sur l’endroit ou sont stockées vos objets, et vous pouvez ainsi disposer d’un vrai tableau de bord pour assigner tel ou tel objet dans tel ou tel conteneur afin d’optimiser vos sites et vos applications web.

Merci de votre lecture, si vous êtes arrivés jusque là…


this class defines where to get all the items used to make the web site work

/// This is where to define the storage of every item

/// </summary>

public class MonContext

{

/// <summary>

/// Stored in the session

/// </summary>

/// <returns></returns>

public static UserProvider GetCurrentUser()

{

StoringContainerProvider container = StoringContainerFactory.GetInstance(StoringContainerFactory.E_StoringContainerType.Session);

if (container.LoadObject(“User”) == null)

{

UserProvider user = new UserProvider();

container.SaveObject(user, “user”);

}

return (UserProvider)container.LoadObject(“User”);

}

}

Articles Précédents »

Catégories

Suivre

Get every new post delivered to your Inbox.