Affichage des articles dont le libellé est Article. Afficher tous les articles
Affichage des articles dont le libellé est Article. Afficher tous les articles

Validation Asp.Net MVC 2.0 avec Entlib 5.0

Asp.Net 2.0 est maintenant pourvu d’un système de validation des entités du modèle très puissant basé sur des attributs qu’il faut placer sur les propriétés d’une classe pour valider celle-ci.

 

Voir sur le site de Scott Guthrie : http://weblogs.asp.net/scottgu/archive/2010/01/15/asp-net-mvc-2-model-validation.aspx

A priori ce dispositif est très pratique, mais à la moindre modification de règle de validation il faut recompiler le projet et mettre à jour l’application web pour que celle-ci soit prise en compte.

Les “géniaux” développeurs d’Asp.Net MVC 2.0 ont mis en place un Fournisseur de validation d’entité qu’il est possible d’étendre.

Je vais montrer ici comment utiliser le Validation Application Block d’Entlib 5.0 pour valider les entités du modèle et stocker les règles de validation dans un fichier de configuration xml a part.

Dans un premier temps il va falloir indiquer que l’on veut utiliser le Validation Application Block d’Entlib

	public class MvcApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);

}

protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();

RegisterRoutes(RouteTable.Routes);

var validationFile = Context.Server.MapPath("/validations.config");

ModelValidatorProviders.Providers.Clear();
ModelValidatorProviders.Providers.Add(new EntLibValidatorProvider(validationFile));
}
}


Il faut donc effacer les validateurs standards, puis ajouter celui d’enlib, j’ai récupéré ce code sur le blog de Brad Wilson à l’adresse suivante :


http://bradwilson.typepad.com/blog/2009/10/enterprise-library-validation-example-for-aspnet-mvc-2.html


	public class EntLibValidatorProvider : ModelValidatorProvider
{
private string m_ConfigFileName;

public EntLibValidatorProvider(string configFileName)
{
m_ConfigFileName = configFileName;
}

public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context)
{
var configSource = new Microsoft.Practices.EnterpriseLibrary.Common.Configuration.FileConfigurationSource(m_ConfigFileName);
var validator = ValidationFactory.CreateValidatorFromConfiguration(metadata.ModelType, "defaultRuleSets", configSource);

if (validator != null)
{
yield return new EntLibValidatorWrapper(metadata, context, validator);
}
}
}


a noter, la fabrique de validator possède une methode qui permet de charger les règles a partir d’un fichier (le chemin de celui-ci est passé en paramètre du constructeur)



voici le code du wrapper EntLibValidatorWrapper



	public class EntLibValidatorWrapper : ModelValidator
{
private Validator _validator;

public EntLibValidatorWrapper(ModelMetadata metadata, ControllerContext context, Validator validator)
: base(metadata, context)
{
_validator = validator;
}

public override IEnumerable<ModelValidationResult> Validate(object container)
{
return ConvertResults(_validator.Validate(Metadata.Model));
}

private IEnumerable<ModelValidationResult> ConvertResults(IEnumerable<ValidationResult> validationResults)
{
if (validationResults != null)
{
foreach (var validationResult in validationResults)
{
if (validationResult.NestedValidationResults != null)
{
foreach (var result in ConvertResults(validationResult.NestedValidationResults))
{
yield return result;
}
}

yield return new ModelValidationResult
{
Message = validationResult.Message,
MemberName = validationResult.Key
};
}
}
}
}




Pour l’exemple l’idée sera de valider une classe très simple Person



	public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}


On peut voir qu’elle ne possède aucun attribut (pure classe poco).



J’ajoute une règle qui interdit que le nom puisse être vide dans un fichier en racine du site web nommé “validations.config”



<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="validation" type="Microsoft.Practices.EnterpriseLibrary.Validation.Configuration.ValidationSettings, Microsoft.Practices.EnterpriseLibrary.Validation, Version=5.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="true" />
</configSections>
<validation>
<type name="MvcEntlibValidation.Models.Person" defaultRuleset="defaultRuleSets"
assemblyName="MvcEntlibValidation, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<ruleset name="defaultRuleSets">
<properties>
<property name="LastName">
<validator type="Microsoft.Practices.EnterpriseLibrary.Validation.Validators.NotNullValidator, Microsoft.Practices.EnterpriseLibrary.Validation, Version=5.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
messageTemplate="Vous devez indiquer un nom" name="Not Null Validator" />
</property>
</properties>
</ruleset>
</type>
</validation>
</configuration>


Entlib 5.0 possède un editeur de configuration assez sympa, voici ce que ça donne :



image 


il ne reste plus qu’a mettre en place l’action d’edition d’une personne dans le controller :


		public ActionResult EditPerson()
{
ViewData.Model = new Models.Person();
return View();
}

[HttpPost]
public ActionResult EditPerson(Models.Person person)
{
ViewData.Model = person;

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

return View();
}


 



L’appel de la propriété ModelState.IsValid a pour effet de valider l’instance de la classe Person.



image



Voici le contenu de la vue :



    <h2>EditPerson</h2>

<% =Html.ValidationSummary() %>
<% Html.BeginForm(); %>

Nom : <%=Html.EditorFor(m => m.LastName) %><br />
<%=Html.ValidationMessageFor(m => m.LastName) %>
<br />

Prénom : <%=Html.EditorFor(m => m.FirstName) %><br />
<%=Html.ValidationMessageFor(m => m.FirstName) %>

<br />

<input type="submit" name="Valider" />

<% Html.EndForm(); %>





Il ne manque plus que la mise en place de la validation client à la volée pour que ce soit parfait, voici le code source de l’exemple :


Utiliser Unity 2 avec Asp.net MVC 2

Le principe MVC pour Asp.Net est un vraie révolution dans le monde Microsoft par rapport aux WebForms, j’ai vraiment été surpris du virage à 180° qui a été initié par les architectes MS, notamment sur l’abandon total du ViewState et du systeme evenementiel  (pas de regret pour moi ;)),  elle l’est encore plus lorsque l’on peut conjuguer ce systeme avec un inverseur de controle type Unity.

J’ai déjà expliqué sur ce blog qu’est que Unity application block et fait une introduction sur la façon de l’utiliser.

Je vais expliquer ici comment se servir de Unity pour faire de l’injection dans les controllers MVC.

Dans un premier temps il faut mettre en place un container Unity et le configurer, le meilleur endroit pour faire ça est de placer dans l’evenement Application Start dans Global.asax par exemple.

 

	public class MvcApplication : System.Web.HttpApplication
{
public static IUnityContainer IoC { get; private set; }

protected void Application_Start()
{
IoC = new UnityContainer();
}


        }



Il existe différentes techniques pour “stocker” l’instance du container , dans cet exemple pour simplifier, je l’ai placé dans une variable statique de la classe MvcApplication, il sera possible de la retrouver facilement via une appel MvcApplication.IoC.



Asp.Net utilise une Factory pour charger les controlleurs qui “matchent” les routes configurées dans un site MVC, il s’agit de System.Web.Mvc.DefaultControllerFactory, il faut dans un premier temps creer un nouveau Type qui hérite de la fabrique de controlleurs par defaut.



	internal class UnityControllerFactory : DefaultControllerFactory
{
public UnityControllerFactory(IUnityContainer container)
{
this.Container = container;
}

protected IUnityContainer Container { get; private set; }

protected override IController GetControllerInstance(System.Web.Routing.RequestContext requestContext, Type controllerType)
{
if (controllerType == null)
{
throw new HttpException(404, String.Format("The controller for path '{0}' could not be found or it does not implement IController.",
requestContext.HttpContext.Request.Path));
}


var controller = Container.Resolve(controllerType) as Controller;
controller.ActionInvoker = new UnityActionInvoker(Container);

return controller;
}
}


Il faut passer le container IoC dans le constructeur de la fabrique, et overrider la methode GetControllerType pour que IoC s’occupe de charger les bons controllers en fonction des routes via :


 var controller = Container.Resolve(controllerType) as Controller; 


la ligne juste en dessous nous permet aussi, si une action d’un controller est marquée par un attribut de type ActionFilterAttribute de pouvoir y injecter aussi des services référencés dans le container, voici le code de l’action Invoker


	public class UnityActionInvoker : ControllerActionInvoker
{
public UnityActionInvoker(IUnityContainer container)
{
this.Container = container;
}

protected IUnityContainer Container { get; private set; }

protected override ActionExecutedContext InvokeActionMethodWithFilters(ControllerContext controllerContext, IList<IActionFilter> filters, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters)
{
foreach (var filter in filters)
{
Container.BuildUp(filter.GetType(), filter);
}

return base.InvokeActionMethodWithFilters(controllerContext, filters, actionDescriptor, parameters);
}
}


De retour dans notre Application_Start, il suffit alors d’indiquer notre nouvelle fabrique de controller au systeme de la manière suivante :


 


		protected void Application_Start() 



              { 


                   IoC = new UnityContainer(); 


	           var cfactory = new UnityControllerFactory(IoC);
ControllerBuilder.Current.SetControllerFactory(cfactory);


              }



 


Unity prendra alors le control sur l’application web et permettra l’injection, l’interception, ect...


Exemple d’injection d’un service de log pour toutes les actions d’un controller marqué par un Attribut.


voici l’interface du logger :


	public interface ILogger
{
void Write(string message);
}


 



Dans un deuxieme temps nous allons creer un Attribut de log pour marquer les actions du controller :



	public class LoggerActionFilterAttribute : ActionFilterAttribute
{
[Dependency]
public Services.ILogger Logger { get; set; }

public override void OnResultExecuted(ResultExecutedContext filterContext)
{
base.OnResultExecuted(filterContext);
var viewResult = filterContext.Result as ViewResult;
Logger.Write(viewResult.ViewName);
}
}


 



Il faut remarquer l’attribut [Dependency] qui marque la propriété Logger de type ILogger, comme évoqué plus haut nous avons également dans la fabrique de controller intercepté la configuration des ActionInvoker via la ligne suivante



controller.ActionInvoker = new UnityActionInvoker(Container);



Grace au mécanisme d’injection de Unity, il affecte la propriété Logger sans qui soit nécéssaire de chercher ou d’instancier son type .



Dans le controller Home par exemple , il faut alors marquer une action pour pouvoir la logger



		[ActionFilters.LoggerActionFilter]
public ActionResult Index()
{
ViewData["Message"] = "Welcome to ASP.NET MVC!";

return View();
}


Bien sur il est aussi possible d’injecter l’instance du logger dans le controller toujours en utilisant Unity , une technique serait par exemple d’ajouter un paramètre dans le constructeur.



	[HandleError]
public class HomeController : Controller
{
public HomeController(Services.ILogger logger)
{
this.Logger = logger;
}

protected Services.ILogger Logger { get; private set; }

[ActionFilters.LoggerActionFilter]
public ActionResult Index()
{
ViewData["Message"] = "Welcome to ASP.NET MVC!";

return View();
}

public ActionResult About()
{
return View();
}
}


Et la il n’y a pas besoin de marquer la propriété via un attribut [Dependency] car Unity sait résoudre le paramètre du constructeur s’il a été défini au préalable de la manière suivante :


		protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();

RegisterRoutes(RouteTable.Routes);

var container = new UnityContainer();

container.RegisterType<Services.ILogger, Services.ConcreteLogger>(new ContainerControlledLifetimeManager());

var cfactory = new UnityControllerFactory(container);
ControllerBuilder.Current.SetControllerFactory(cfactory);
}


Voila, avec le couple Asp.Net MVC + Unity il devient vraiment possible de créer de très grandes applications web très agiles, dans un prochain billet, j’expliquerai comment mettre en place un dispositif de plugins toujours basé sur Unity pour asp.net MVC

Comment traduire un site Asp.net MVC ?

 

La traduction d’un site en plusieurs langues est une tache difficile, et les façons de faire sont nombreuses, je vais proposer ici une methode qui permet de traduire “à la volée” un site web réalisé avec la technologie asp.net MVC.

Le problème principal pour un traducteur, est d’obtenir le contexte de traduction (la page dans laquelle se trouve le texte et son ensemble), la traduction mot à mot engendre systematiquement des aproximations. un autre problème est le format d’echange avec le traducteur, puis la réintroduction des textes traduits. Bref la galère…

Le problème principal pour un webdesigner est qu’il doit editer des fichiers de ressources, bien localiser les textes dans les pages puis trouver leurs correspondances dans des fichier généralement en xml. Bref la galère…

Pour resoudre les principaux problèmes du traducteur, je propose qu’il puisse traduire directement dans la page comme un wiki. Ce qui supprime pratiquement le problème du webdesigner et de ses fichiers, il reste néanmoins la definitions des textes dans les pages a mettre en place, pour cela je propose l’ecriture suivante :

Il s’agit de la page index.aspx par defaut lorsque l’on crée un nouveau projet de type asp.net mvc

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="indexTitle" ContentPlaceHolderID="TitleContent" runat="server">
Home Page
</asp:Content>

<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
<h2><%= Html.Encode(ViewData["Message"]) %></h2>
<p>
<%=Html.Localize("Content1","To learn more about ASP.NET MVC visit")%><a href="http://asp.net/mvc" title="ASP.NET MVC Website">http://asp.net/mvc</a>.
</p>
</asp:Content>



Concernant le fichier index.aspx, l’idée est de sauvegarder un fichier par language avec le nom suivant :



index.aspx.fr.resx



index.aspx.en.resx



ect…



le contenu des fichiers est le suivant :




<?xml version="1.0" encoding="utf-8"?>
<localizations>
<localization key="Title"><![CDATA[Mon Application MVC]]></localization>
<localization key="Content1"><![CDATA[Pour en apprendre d'avantage a propos d'ASP.Net MVC allez sur]]></localization>
</localizations>



On retrouve l’identifiant “Content1”



Le but de la methode d’extension Localize(..) est de reconnaitre le language en cours et de retrouver le bon terme.



voici le détail de cette methode :




public static string Localize(this HtmlHelper helper, string contentKey, string defaultContent)
{
string translationLink = null;
var localizationService = new Services.LocalizationService();
var view = helper.ViewContext.View as System.Web.Mvc.WebFormView;
var path = view.ViewPath.Replace("~", "");
if (helper.ViewContext.HttpContext.Request["translate"] == "1")
{
translationLink = helper.RouteLink("translate"
, new { controller = "Localization"
, action = "LocalizeContent"
, id = string.Empty
, path = path
, key = contentKey
});
}
if (view != null)
{
var localization = localizationService.GetContentLocalization(path, contentKey);
if (localization != null)
{
return localization.Value + "&nbsp;" + translationLink + "&nbsp;";
}
}

return defaultContent + "&nbsp;" + translationLink + "&nbsp;";
}



Dans un premier temps elle va instancier le service de traduction, recuperer le nom du fichier en cours et interroger le service de traduction pour savoir s’il existe un terme pour le fichier en cours avec l’identifiant contentKey , si c’est le cas le terme est retourné, sinon on utilise le terme defaultContent.



On peut voir aussi que l’on verifie l’existence d’un parametre translate=1 dans l’url, c’est ce parametre qui permet de traduire les pages à la volée, s’il est la, on ajoute un lien pour la traduction du terme a coté du terme a traduire, de telle sorte que le traducteur retrouve son contexte.



Voici le service de traduction :




public class LocalizationService : ILocalizationService
{
public LocalizationService()
{

}

private List<Models.ContentLocalization> m_LocalizationList;
private static object m_Lock = new object();

public IList<Models.ContentLocalization> LocalizationList
{
get
{
if (m_LocalizationList == null)
{
lock (m_Lock)
{
if (m_LocalizationList == null)
{
m_LocalizationList = new List<Models.ContentLocalization>();
var currentDirectory = System.Web.HttpContext.Current.Server.MapPath("/");
var fileList = from file in System.IO.Directory.GetFiles(currentDirectory, "*.resx", System.IO.SearchOption.AllDirectories)
select file;

foreach (var file in fileList)
{
XDocument doc = null;
string content = null;
System.IO.StreamReader fs = null;
try
{
fs = System.IO.File.OpenText(file);
content = fs.ReadToEnd();
doc = XDocument.Parse(content);
}
catch
{
continue;
}
finally
{
if (fs != null)
{
fs.Close();
}
}

var path = System.IO.Path.GetFileName(file);
var language = path.Split('.')[2];
path = file.Replace(string.Format(".{0}.resx", language), "");
path = path.Replace(currentDirectory, "");
path = '/' + path.Replace('\\', '/');
m_LocalizationList.AddRange(from x in doc.Root.Elements()
select new Models.ContentLocalization()
{
Key = x.Attribute(XName.Get("key")).Value,
Value = x.Value,
Path = path,
Language = language,
});

}
}
}

}
return m_LocalizationList;
}
}

public static string DefaultLanguage
{
get
{
var language = (string)System.Web.HttpContext.Current.Session["language"];
if (language == null)
{
language = "fr";
}
return language;
}
set
{
System.Web.HttpContext.Current.Session["language"] = value;
}
}

#region ILocalizationService Members

/// <summary>
/// Gets the content localization.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="key">The key.</param>
/// <returns></returns>
public Models.ContentLocalization GetContentLocalization(string path, string key)
{
return LocalizationList.FirstOrDefault(i => i.Path.Equals(path, StringComparison.InvariantCultureIgnoreCase)
&& i.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)
&& i.Language.Equals(LocalizationService.DefaultLanguage, StringComparison.InvariantCultureIgnoreCase));
}

public IList<Models.ContentLocalization> GetContentLocalizationList(string path, string key)
{
var list = LocalizationList.Where(i => i.Path.Equals(path, StringComparison.InvariantCultureIgnoreCase)
&& i.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)).ToList();

foreach (var language in GetLanguageList())
{
var item = list.SingleOrDefault(i => i.Language.Equals(language, StringComparison.InvariantCultureIgnoreCase));
if (item == null)
{
list.Add(new Models.ContentLocalization()
{
Key = key,
Path = path,
Language = language,
Value = null,
});
}
}

return list;
}

/// <summary>
/// Sauvegarde une liste de traduction.
///
/// le principe est de sauvegarder un fichier par langue pour une page ou un controle donné
///
/// par exemple si le path est :
///
/// /views/home/index.aspx
///
/// la liste des traductions est fr, en alors, il y a creation ou append de 2 fichiers
///
/// /views/home/index.aspx.fr.resx
/// /views/home/index.aspx.en.resx
///
/// </summary>
/// <param name="list">The list.</param>
public void Save(IList<Models.ContentLocalization> list)
{
var rootPath = System.Web.HttpContext.Current.Server.MapPath("/");

foreach (var localization in list)
{
if (string.IsNullOrEmpty( localization.Value))
{
continue;
}
var path = System.IO.Path.Combine(rootPath, localization.Path.Trim('
/').Replace('/', '\\'));
path = string.Format("{0}.{1}.resx", path, localization.Language);

XDocument doc = null;
if (System.IO.File.Exists(path))
{
using (var fs = System.IO.File.OpenText(path))
{
doc = XDocument.Parse(fs.ReadToEnd());
fs.Close();
}
var match = doc.XPathSelectElement(string.Format("//localization[@key='
{0}']", localization.Key));
if (match != null)
{
match.RemoveAll();
match.Add(new XCData(localization.Value));
match.Add(new XAttribute("
key", localization.Key));
}
else
{
var elem = new XElement("
localization", new XCData(localization.Value));
elem.Add(new XAttribute("
key", localization.Key));
doc.Root.Add(elem);
}
}
else
{
doc = new XDocument(new XDeclaration("
1.0", "UTF-8", null));
var root = new XElement("
localizations");
var elem = new XElement("
localization", new XCData(localization.Value));
elem.Add(new XAttribute("
key", localization.Key));
root.Add(elem);
doc.Add(root);
}
doc.Save(path);
}
m_LocalizationList = null;
}

public IEnumerable<string> GetLanguageList()
{
yield return "
fr";
yield return "
en";
yield return "
es";
yield break;
}

#endregion
}







il implemente l’interface suivante :




public interface ILocalizationService
{
/// <summary>
/// Retourne une traduction en fonction du chemin de la vue et de la clé de traduction
/// </summary>
/// <param name="path">The path.</param>
/// <param name="key">The key.</param>
/// <returns></returns>
Models.ContentLocalization GetContentLocalization(string path, string key);
/// <summary>
/// Retourne la liste de toutes les traductions possible d'un contenu
/// </summary>
/// <param name="path">The path.</param>
/// <param name="key">The key.</param>
/// <returns></returns>
IList<Models.ContentLocalization> GetContentLocalizationList(string path, string key);

/// <summary>
/// Sauvegarde une liste de traduction.
/// </summary>
/// <param name="list">The list.</param>
void Save(IList<Models.ContentLocalization> list);

/// <summary>
/// Liste des language de traduction
/// </summary>
/// <returns></returns>
IEnumerable<string> GetLanguageList();
}





Comme je l’ai évoqué , si le parametre translate=1 est present, des liens son générés dans la page, ces liens utilisent le controlleur de traduction dont voici le code :




public class LocalizationController : Controller
{
public LocalizationController()
{
this.LocalizationService = new Services.LocalizationService();
}

protected Services.ILocalizationService LocalizationService { get; set; }

public ViewResult LocalizeContent(string path, string key)
{
var list = LocalizationService.GetContentLocalizationList(path, key);
ViewData.Model = list;
ViewData["Key"] = key;
ViewData["Path"] = path;
return View();
}

public ViewResult SaveLocalizedContent(FormCollection form)
{
string key = form["Key"];
string path = form["Path"];
var list = LocalizationService.GetContentLocalizationList(path, key);
var languages = form["lg"].Split(',');
var values = form["value"].Split(',');

for (int i = 0; i < languages.Length; i++)
{
var language = languages[i];
var value = values[i];
var localization = list.Single(l => l.Language.Equals(language, StringComparison.InvariantCultureIgnoreCase));
localization.Value = value;
}

LocalizationService.Save(list);

ViewData.Model = list;
ViewData["Key"] = key;
ViewData["Path"] = path;

return View("LocalizeContent");
}

}





Je n’ai pas évoqué la securité, mais bien sur il ne faut pas utiliser cette solution telle qu’elle, en effet il ne faut pas qu’un internaute puisse retraduire le site, mais on peut s’appuyer facilement sur les membership asp.net, ajouter un attribut “Authorize” sur les methodes du controlleur de traduction.



Pour la demo, il faut que le user sous lequel tourne le site web ai les droits d’ecriture dans le repertoire et sous-repertoire views.



pour voir plus en détail cette solution , voici le code source :


Comment faire un handler d’image en asp.net mvc

 

ASP.NET MVC

Lors de la création d’un site web un peu compliqué ou par exemple les images se trouvent sous forme de blob dans une base de données, ou les fichiers images existent à differents endroits, et pas mal d’autre cas encore, il faut utiliser un handler d’image, qui permet d’intercepter la requete de de produire un traitement avant de rendre l’image demandée.

Avec asp.net version webforms il fallait declarer un handler, souvent une url de la forme http://www.monsite.com/image.ashx?fileName=image.gif, ou plus seo compliant http://www.monsite.com/image.ashx/image.gif (malheureusement cette forme d’url ne fonctionnant pas avec le server web de dev cassini).

Avec asp.net mvc l’idée est de faire passer toutes les requetes via le “ControllerFactory”, il faut ajouter une route de la forme :

routes.MapRoute(
"Images"
,"images/{*fileName}"
, new { controller = "Images", action = "Index" }
);

L’url peut etre de la forme http://www.monsite.com/images/image.gif, attention il ne faut pas que le repertoire images existe à la racine du site.


 


A savoir, {*imageFilename} permet d’ajouter ce que l’on veut comme nom de fichier et par exemple /folder1/folder2/image.gif


 


Le controller est codé de la manière suivante :


 



public class ImagesController : Controller
{
public ActionResult Index(string fileName)
{
fileName = System.IO.Path.Combine(@"c:\images", fileName);
var mimeType = "image/gif";
return this.File(fileName, mimeType);
}
}



Pour ne pas compliquer le discours, je suis parti du principe que les images se trouvent dans le repertoire c:\images et qu’il n’y a que des fichiers gif, l’interception est la combinaison du nom de fichier et son chemin.



Le problème ici est qu’il manque le type mime, voici ci dessous un petit dictionnaire qui permet de retrouver un mimeType via une extension ou vice/versa.




public static Dictionary<string, string> MIMETypeDictionary
= new Dictionary<string, string>()
{
{"323", "text/h323"},
{"3gp", "video/3gpp"},
{"3gpp", "video/3gpp"},
{"acp", "audio/x-mei-aac"},
{"act", "text/xml"},
{"actproj", "text/plain"},
{"ade", "application/msaccess"},
{"adp", "application/msaccess"},
{"ai", "application/postscript"},
{"aif", "audio/aiff"},
{"aifc", "audio/aiff"},
{"aiff", "audio/aiff"},
{"asf", "video/x-ms-asf"},
{"asm", "text/plain"},
{"asx", "video/x-ms-asf"},
{"au", "audio/basic"},
{"avi", "video/avi"},
{"bmp", "image/bmp"},
{"bwp", "application/x-bwpreview"},
{"c", "text/plain"},
{"cat", "application/vnd.ms-pki.seccat"},
{"cc", "text/plain"},
{"cdf", "application/x-cdf"},
{"cer", "application/x-x509-ca-cert"},
{"cod", "text/plain"},
{"cpp", "text/plain"},
{"crl", "application/pkix-crl"},
{"crt", "application/x-x509-ca-cert"},
{"cs", "text/plain"},
{"css", "text/css"},
{"csv", "application/vnd.ms-excel"},
{"cxx", "text/plain"},
{"dbs", "text/plain"},
{"def", "text/plain"},
{"der", "application/x-x509-ca-cert"},
{"dib", "image/bmp"},
{"dif", "video/x-dv"},
{"dll", "application/x-msdownload"},
{"doc", "application/msword"},
{"dot", "application/msword"},
{"dsp", "text/plain"},
{"dsw", "text/plain"},
{"dv", "video/x-dv"},
{"edn", "application/vnd.adobe.edn"},
{"eml", "message/rfc822"},
{"eps", "application/postscript"},
{"etd", "application/x-ebx"},
{"etp", "text/plain"},
{"exe", "application/x-msdownload"},
{"ext", "text/plain"},
{"fdf", "application/vnd.fdf"},
{"fif", "application/fractals"},
{"fky", "text/plain"},
{"gif", "image/gif"},
{"gz", "application/x-gzip"},
{"h", "text/plain"},
{"hpp", "text/plain"},
{"hqx", "application/mac-binhex40"},
{"hta", "application/hta"},
{"htc", "text/x-component"},
{"htm", "text/html"},
{"html", "text/html"},
{"htt", "text/webviewhtml"},
{"hxx", "text/plain"},
{"i", "text/plain"},
{"iad", "application/x-iad"},
{"ico", "image/x-icon"},
{"ics", "text/calendar"},
{"idl", "text/plain"},
{"iii", "application/x-iphone"},
{"inc", "text/plain"},
{"infopathxml", "application/ms-infopath.xml"},
{"inl", "text/plain"},
{"ins", "application/x-internet-signup"},
{"iqy", "text/x-ms-iqy"},
{"isp", "application/x-internet-signup"},
{"java", "text/java"},
{"jnlp", "application/x-java-jnlp-file"},
{"jpg", "image/jpeg"},
{"jpe", "image/jpeg"},
{"jpeg", "image/jpeg"},
{"jfif", "image/jpeg"},
{"jsl", "text/plain"},
{"kci", "text/plain"},
{"la1", "audio/x-liquid-file"},
{"lar", "application/x-laplayer-reg"},
{"latex", "application/x-latex"},
{"lavs", "audio/x-liquid-secure"},
{"lgn", "text/plain"},
{"lmsff", "audio/x-la-lms"},
{"lqt", "audio/x-la-lqt"},
{"lst", "text/plain"},
{"m1v", "video/mpeg"},
{"m3u", "audio/mpegurl"},
{"m4e", "video/mpeg4"},
{"MAC", "image/x-macpaint"},
{"mak", "text/plain"},
{"man", "application/x-troff-man"},
{"map", "text/plain"},
{"mda", "application/msaccess"},
{"mdb", "application/msaccess"},
{"mde", "application/msaccess"},
{"mdi", "image/vnd.ms-modi"},
{"mfp", "application/x-shockwave-flash"},
{"mht", "message/rfc822"},
{"mhtml", "message/rfc822"},
{"mid", "audio/mid"},
{"midi", "audio/mid"},
{"mk", "text/plain"},
{"mnd", "audio/x-musicnet-download"},
{"mns", "audio/x-musicnet-stream"},
{"MP1", "audio/mp1"},
{"mp2", "video/mpeg"},
{"mp2v", "video/mpeg"},
{"mp3", "audio/mpeg"},
{"mp4", "video/mp4"},
{"mpa", "video/mpeg"},
{"mpe", "video/mpeg"},
{"mpeg", "video/mpeg"},
{"mpf", "application/vnd.ms-mediapackage"},
{"mpg", "video/mpeg"},
{"mpg4", "video/mp4"},
{"mpga", "audio/rn-mpeg"},
{"mpv2", "video/mpeg"},
{"NMW", "application/nmwb"},
{"nws", "message/rfc822"},
{"odc", "text/x-ms-odc"},
{"odh", "text/plain"},
{"odl", "text/plain"},
{"p10", "application/pkcs10"},
{"p12", "application/x-pkcs12"},
{"p7b", "application/x-pkcs7-certificates"},
{"p7c", "application/pkcs7-mime"},
{"p7m", "application/pkcs7-mime"},
{"p7r", "application/x-pkcs7-certreqresp"},
{"p7s", "application/pkcs7-signature"},
{"PCT", "image/pict"},
{"pdf", "application/pdf"},
{"pdx", "application/vnd.adobe.pdx"},
{"pfx", "application/x-pkcs12"},
{"pic", "image/pict"},
{"PICT", "image/pict"},
{"pko", "application/vnd.ms-pki.pko"},
{"png", "image/png"},
{"pnt", "image/x-macpaint"},
{"pntg", "image/x-macpaint"},
{"pot", "application/vnd.ms-powerpoint"},
{"ppa", "application/vnd.ms-powerpoint"},
{"pps", "application/vnd.ms-powerpoint"},
{"ppt", "application/vnd.ms-powerpoint"},
{"prc", "text/plain"},
{"prf", "application/pics-rules"},
{"ps", "application/postscript"},
{"pub", "application/vnd.ms-publisher"},
{"pwz", "application/vnd.ms-powerpoint"},
{"qt", "video/quicktime"},
{"qti", "image/x-quicktime"},
{"qtif", "image/x-quicktime"},
{"qtl", "application/x-quicktimeplayer"},
{"qup", "application/x-quicktimeupdater"},
{"r1m", "application/vnd.rn-recording"},
{"r3t", "text/vnd.rn-realtext3d"},
{"RA", "audio/vnd.rn-realaudio"},
{"RAM", "audio/x-pn-realaudio"},
{"rat", "application/rat-file"},
{"rc", "text/plain"},
{"rc2", "text/plain"},
{"rct", "text/plain"},
{"rec", "application/vnd.rn-recording"},
{"rgs", "text/plain"},
{"rjs", "application/vnd.rn-realsystem-rjs"},
{"rjt", "application/vnd.rn-realsystem-rjt"},
{"RM", "application/vnd.rn-realmedia"},
{"rmf", "application/vnd.adobe.rmf"},
{"rmi", "audio/mid"},
{"RMJ", "application/vnd.rn-realsystem-rmj"},
{"RMM", "audio/x-pn-realaudio"},
{"rms", "application/vnd.rn-realmedia-secure"},
{"rmvb", "application/vnd.rn-realmedia-vbr"},
{"RMX", "application/vnd.rn-realsystem-rmx"},
{"RNX", "application/vnd.rn-realplayer"},
{"rp", "image/vnd.rn-realpix"},
{"RPM", "audio/x-pn-realaudio-plugin"},
{"rqy", "text/x-ms-rqy"},
{"rsml", "application/vnd.rn-rsml"},
{"rt", "text/vnd.rn-realtext"},
{"rtf", "application/msword"},
{"rul", "text/plain"},
{"RV", "video/vnd.rn-realvideo"},
{"s", "text/plain"},
{"sc2", "application/schdpl32"},
{"scd", "application/schdpl32"},
{"sch", "application/schdpl32"},
{"sct", "text/scriptlet"},
{"sd2", "audio/x-sd2"},
{"sdp", "application/sdp"},
{"sit", "application/x-stuffit"},
{"slk", "application/vnd.ms-excel"},
{"sln", "application/octet-stream"},
{"SMI", "application/smil"},
{"smil", "application/smil"},
{"snd", "audio/basic"},
{"snp", "application/msaccess"},
{"spc", "application/x-pkcs7-certificates"},
{"spl", "application/futuresplash"},
{"sql", "text/plain"},
{"srf", "text/plain"},
{"ssm", "application/streamingmedia"},
{"sst", "application/vnd.ms-pki.certstore"},
{"stl", "application/vnd.ms-pki.stl"},
{"swf", "application/x-shockwave-flash"},
{"tab", "text/plain"},
{"tar", "application/x-tar"},
{"tdl", "text/xml"},
{"tgz", "application/x-compressed"},
{"tif", "image/tiff"},
{"tiff", "image/tiff"},
{"tlh", "text/plain"},
{"tli", "text/plain"},
{"torrent", "application/x-bittorrent"},
{"trg", "text/plain"},
{"txt", "text/plain"},
{"udf", "text/plain"},
{"udt", "text/plain"},
{"uls", "text/iuls"},
{"user", "text/plain"},
{"usr", "text/plain"},
{"vb", "text/plain"},
{"vcf", "text/x-vcard"},
{"vcproj", "text/plain"},
{"viw", "text/plain"},
{"vpg", "application/x-vpeg005"},
{"vspscc", "text/plain"},
{"vsscc", "text/plain"},
{"vssscc", "text/plain"},
{"wav", "audio/wav"},
{"wax", "audio/x-ms-wax"},
{"wbk", "application/msword"},
{"wiz", "application/msword"},
{"wm", "video/x-ms-wm"},
{"wma", "audio/x-ms-wma"},
{"wmd", "application/x-ms-wmd"},
{"wmv", "video/x-ms-wmv"},
{"wmx", "video/x-ms-wmx"},
{"wmz", "application/x-ms-wmz"},
{"wpl", "application/vnd.ms-wpl"},
{"wprj", "application/webzip"},
{"wsc", "text/scriptlet"},
{"wvx", "video/x-ms-wvx"},
{"XBM", "image/x-xbitmap"},
{"xdp", "application/vnd.adobe.xdp+xml"},
{"xfd", "application/vnd.adobe.xfd+xml"},
{"xfdf", "application/vnd.adobe.xfdf"},
{"xla", "application/vnd.ms-excel"},
{"xlb", "application/vnd.ms-excel"},
{"xlc", "application/vnd.ms-excel"},
{"xld", "application/vnd.ms-excel"},
{"xlk", "application/vnd.ms-excel"},
{"xll", "application/vnd.ms-excel"},
{"xlm", "application/vnd.ms-excel"},
{"xls", "application/vnd.ms-excel"},
{"xlt", "application/vnd.ms-excel"},
{"xlv", "application/vnd.ms-excel"},
{"xlw", "application/vnd.ms-excel"},
{"xml", "text/xml"},
{"xpl", "audio/scpls"},
{"xsl", "text/xml"},
{"z", "application/x-compress"},
{"zip", "application/x-zip-compressed"}
};





Le code devient alors :




public ActionResult Index(string fileName)
{
fileName = System.IO.Path.Combine(@"c:\images", fileName);
var ext = System.IO.Path.GetExtension(fileName);
var mime = MIMETypeDictionary.FirstOrDefault(i => i.Key.Equals(ext, StringComparison.InvariantCultureIgnoreCase));
return this.File(fileName, mime.Value);
}

A noter il est possible de renvoyer un tableau de byte à la place d’un fichier, d’autre part si IIS6 est utilisé pour héberger le site il faut soit declarer chaque extension d’application possible par type d’image, ou ajouter un wildcard de la manière suivante :


 


image

La fabrique logicielle Serialcoder

 

Je vais discuter ici de la mise à jour d’un de nos logiciels et de la façon dont nous procedons pour que l’utilsateur final puisse en beneficier en toute quiettude.

La contrainte au depart est qu’un client peut avoir ses propres composants, chaque client n’a pas obligatoirement la meme version qu’un autre, un client peut utiliser un serveur mutualisé, ou son propre serveur en local, voir utiliser une machine soho. Nous utilisons une architecture N-Tiers, sans trop entrer dans le détail, le logiciel est composé des elements suivants qu’il faut deployer de manière synchronisée :

  • La base de données
  • La ou les interfaces utilisateurs
  • Les services
  • Les webservices
  • Les sites web

La base de données

La problèmatique pour nous est de garantir que chacun de nos clients utilise le bon schema (Table/Vue/Procedures stockées) , Il n’etait pas question pour nous de passer les scripts à la main, alors pour etre certain que le schema sera bien mis à jour nous avons developpé un outil qui nous avons nommé “Sql Schema Script Synchronizer”, il s’agit d’un dispositif qui historise tous les scripts de modification du schema SQL, script par script en donnant pour chacun un numéro de version incrémentiel, lors d’une mise a jour il ne nous reste plus qu’à recreer entre une version et une autre une liste de script à passer sur la base de destination pour obtenir la version du schema voulue, ci dessous le workflow que nous utilisons :

Nous mettrons bientot cet outil gratuitement avec son code source a disposition de ceux qui sont interessés par cette problèmatique , le projet sera hebergé sur Google code à l’adresse suivante http://code.google.com/p/s-cube/

 

La ou les interfaces utilisateurs (Client riche)

Concernant le deploiement de cette partie, Microsoft à mis au point la technologie Clickonce, elle permet une fois le logiciel installé sur le poste du client final, de lui indiquer nativement qu’une mise à jour est disponible et qu’il procede a son téléchargement. Cette technologie est très pratique et permet proprement de gerer un grand parc de machine en obligeant le téléchargement sur une version précise (contrairement à un MSI). Le seul bemol est qu’elle ne permet pas nativement le deploiement selectif, dans notre cas certain de nos clients utilisent des composants spécifiques, nous avons du ecrire un outil de publication clickonce qui permet de réaliser cette opération. De plus nous avons décidé que chaque client devait posseder sa propre url personnalisée de téléchargement du client riche.

Les services

La mise a jour des assemblies d’un service NT n’est pas une chose aisée de manière automatisée, autant il existe Clickonce pour les clients riches autant il n’existe rien pour les services NT si ce n’est Stopper celui-ci et installer les assemblies à la main. Cette manipulation est envisageable dans le cas d’un nombre restreint de serveurs et il faut avoir le droit d’y acceder ce qui n’est pas toujours le cas. Pour la mise en place d’un update automatisé des services, nous avons ecrit un premier service NT lui meme dont le role est de mettre a jour d’autre service, nous l’appelons le Service Updater il s’auto update via chargement / déchargement d’assemblies dans un appdomain, son principe est très simple, il check à interval régulier si une mise à jour est disponible sur le repository central, si c’est le cas il commence le téléchargement des assemblies disponibles dans un cache local, puis stoppe les services NT concernés, copie les nouvelles assemblies et redemarre les services.

Les webservices , Les sites web

La copie a chaud est prevu en revanche pour les sites web, au préalable le service updater ayant téléchargé les pages et autres assemblies copie celles-ci directement dans les sites (le plus vite possible) avant cette opération une page de maintenance est activée, a la fin de la copie le site revient dans son mode initial.

Voici le schema global de deploiement :

Deploy

La software factory utilisée basé sur les elements clés suivant :

  • Repository Subversion
  • Cruise Control
  • Sql Schema Script Synchronizer
  • Clickonce
  • Un Service Updater

Les developpeurs envoient leur code source vers un repository Subversion , celui-ci est analysé et buildé sur un serveur d’intégration en utilisant Cruise Control, cela nous permet de proceder aux builds, et de lancer les tests unitaires pour savoir si le code commité est viable.

Dès que nous considerons que le code est stable et que les fonctionnalités définies dans notre roadmap sont bien implémentées et conformes, le chef de produit decide de publier une nouvelle version pour un client spécifique ou tous les clients.

En fonction des clients , des assemblies custom peuvent alors etre buildée en supplément.

Un service de publication prépare les fichiers sur un serveur repository et indique à un web service qu’une nouvelle version est disponible.

Le service Updateur detecte alors le changement et commence le téléchargement des fichiers disponibles et les places dans un cache. S’il detecte qu’il y a un changement de schema de la base de données, alors il place celle ci en maintenance et demande ainsi à tout les clients connectés de bien vouloir se deconnecter avant un labs de temps défini, a l’issue le service updateur coupe tous les services NT concernés, fait un backup de la base et installe ensuite le ou les nouveaux scripts sql jusqu’a atteindre la version voulue par le chef de produit. Il copie alors les nouvelles assemblies pour les services NT et pour les sites web (Extranet, WebService, Ect..).

Quand cette opération est terminée le service updateur indique au repository central qu’il a terminé toutes ses opérations, le repository change la version Clickonce et les clients utilisateurs finaux peuvent sont notifiés que la nouvelle version de leur interface est disponible.

L’utilisateur final est alors completement mis a jour et peut nous demander de nouvelles fonctionnalités ;).

Cette architecture de deploiement nous permet une veritable integration continue, a l’heure actuelle nous publions une à deux versions par mois, si un bug severe se presente nous somme en mesure de le corriger et de deployer une nouvelle version dans les 24 heures qui suivent.