vendredi, mai 3 2013

Event avec arguments sous JQuery

Il est parfois utile de pouvoir ajouter des arguments dans un event JQuery ... Voici comment le réaliser simplement ....

La première chose a réaliser et même la seule est d'avoir notre objet que l'on passera en premier argument de notre event Click. Dans ce cas-ci, nous avons une propriété "myValue" renvoyant un "Hello World"! ... Il suffit ensuite d'utiliser la propriété e.data.myValue pour récupérer notre valeur passée en argument. Voici l'exemple ....
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Hello world!</title>
    <script src="Scripts/jquery-2.0.0.min.js" type="text/javascript"></script>
    <script type="text/javascript">
        $(function () {
            var value = "Hello World!";
            $('#btn').click({ myValue: value }, HelloWorld);
        });
        function HelloWorld(event) {
            alert(event.data.myValue);
        }
    </script>
</head>
<body>
    <input type="button" id="btn" value="Hello" />
</body>
</html>

mercredi, avril 10 2013

Tableau de 2000 lignes avec un bouton ... et la c'est le drame en ASP.NET ... Optimisons le tout !

Beaucoup de personnes disent toujours que ASP.NET est lourd et ne génère pas de beau code HTML... Je reste persuadé que la mauvaise utilisation que l'on fait de ASP.NET pousse à ce genre de problème ... Voici un exemple simple ... Nous avons un cache objet à un certain endroit et nous avons une grille de 2000 lignes. Chaque ligne contient un simple libellé et un bouton qui exécutera une action sur cette ligne ... Nous pensons directement à Repeater / Label et Button ... Et la c'est le drame .... Nous avons un ViewState énorme et une page de très mauvaise qualité ... Il est donc temps de "penser" ... alors voici une autre idée ...
Pourquoi ne pas créer un contrôle Web sans partir sur des CompositeControl mais plutôt sur un simple WebControl. Nous utilisons donc un div pour l'exemple .... 
public class CustomWebControl : WebControl
{
    protected override HtmlTextWriterTag TagKey
    {
        get { return HtmlTextWriterTag.Div; }
    }
    protected override void RenderContents(HtmlTextWriter writer)
    {
        base.RenderContents(writer);
    }
}
La seconde étape est de générer notre code HTML.
public class CustomWebControl : WebControl
{
    protected override HtmlTextWriterTag TagKey
    {
        get { return HtmlTextWriterTag.Div; }
    }
    public MySource DataSource
    {
        get { return (MySource)this.ViewState["DataSource"]; }
        set { this.ViewState["DataSource"] = value; }
    }
    protected override void RenderContents(HtmlTextWriter writer)
    {
        writer.AddAttribute(HtmlTextWriterAttribute.Type, "text");
        writer.AddAttribute(HtmlTextWriterAttribute.Value, this.DataSource.Name);
        writer.RenderBeginTag("input");
        writer.RenderEndTag();
        writer.AddAttribute(HtmlTextWriterAttribute.Type, "button");
        writer.AddAttribute(HtmlTextWriterAttribute.Value, "Hello!");
        writer.RenderBeginTag("input");
        writer.RenderEndTag();
        base.RenderContents(writer);
    }
}
La dernière étape revient a utiliser l'interface IPostbackEventHandler. Celle-ci nous offre la gestion du PostBack.
public class CustomWebControl : WebControlIPostBackEventHandler
{
    protected override HtmlTextWriterTag TagKey
    {
        get { return HtmlTextWriterTag.Div; }
    }
    public MySource DataSource
    {
        get { return (MySource)this.ViewState["DataSource"]; }
        set { this.ViewState["DataSource"] = value; }
    }
    protected override void OnPreRender(EventArgs e)
    {
        base.OnPreRender(e);
        this.Page.RegisterRequiresRaiseEvent(this);
    }
    protected override void RenderContents(HtmlTextWriter writer)
    {
        writer.AddAttribute(HtmlTextWriterAttribute.Type, "text");
        writer.AddAttribute(HtmlTextWriterAttribute.Value, this.DataSource.Name);
        writer.RenderBeginTag("input");
        writer.RenderEndTag();
        writer.AddAttribute(HtmlTextWriterAttribute.Type, "button");
        writer.AddAttribute(HtmlTextWriterAttribute.Value, "Edit!");
        writer.AddAttribute(HtmlTextWriterAttribute.Onclick,
            this.Page.ClientScript.GetPostBackEventReference(this"Edit;" + this.DataSource.Id));
        writer.RenderBeginTag("input");
        writer.RenderEndTag();
        base.RenderContents(writer);
    }
    public void RaisePostBackEvent(string eventArgument)
    {
    }
}
Il est dès lors possible d'ajouter sur notre contrôle un event et de le lancer dans la méthode RaisePostBackEvent. N'ayant pas besoin de ViewState, on peut très bien au Load de la page remplir le Datasource d'un Repeater ... Le rendu sera parfait et on supportera la fonctionnalité ...
<asp:Repeater runat="server" ID="repeater">
    <ItemTemplate>
        <cc1:CustomWebControl ID="control" runat="server" EnableViewState="False" />
    </ItemTemplate>
</asp:Repeater>
Ceci termine ce petit article... Il offre la possibilité si le contrôle est un affichage de données sans modification de s'abstraire facilement du ViewState,de réduire le code HTML généré et d'éviter une profusion de contrôle ASP.NET sur une page ... En effet, il ne faut pas oublier qu'il y a par défaut une limite (modifiable dans la config mais si ce n'est pas indispensable ...) ...

lundi, février 25 2013

Custom WebControl avec des events en Javascript

Après avoir expliqué comment réaliser un contrôle custom ASP.NET dans un précédent post, nous allons expliquer comment ajouter des "events" dans nos classes. Cela nous permettra de nous abonner et de réaliser des actions en conséquence.

Pour ce faire, il faut rajouter un event dans notre code embedded JavaScript.

_onchange: function (e) {
    var value = parseFloat(e.target.value);
    var isIncorrect = false;
    if (isNaN(value) || !isFinite(value)) {
        isIncorrect = true;
    }
    isIncorrect = isIncorrect || value > 100 || value < 0;
    if (isIncorrect) {
        if (this._errorCssClass !== null && this._errorCssClass !== '') {
            this.addCssClass(this._errorCssClass);
        }
    }
    else {
        this.removeCssClass(this._errorCssClass);
    }
    this._throw_change(isIncorrect);
},
add_change: function (handler) {
    this.get_events().addHandler('change', handler);
},
remove_change: function (handler) {
    this.get_events().removeHandler('change', handler);
},

_throw_change: function (eventArgs) {
    var handler = this.get_events().getHandler('change');
    if (handler) handler(this, eventArgs);
}

Ensuite, nous allons enregistrer une action à exécuter au load. Ce code JavaScript doit se situer obligatoirement en dessous du ScriptManager.

<asp:ScriptManager ID="ScriptManager1" runat="server" />
<script type="text/javascript" language="javascript">
    Sys.Application.add_load(LoadEvents);
</script>
function LoadEvents() {
    var elt = $find("UserControlTest1_PercentageControl1");
    elt.add_change(OnChange);
}

Nous pouvons maintenant au load nous abonner à l'event et réaliser une action. Ici nous souhaitons par exemple désactiver le bouton si la valeur de la textbox est invalide. Cela évite tout click (et donc retour serveur etc) en cas d'erreur d'input.

function OnChange(source, eventArgs) {
    var elt = $get("UserControlTest1_Button1");
    elt.disabled = eventArgs === true ? true : false;
}

Nous pouvons tester et nous voyons bien notre bouton se désactiver lorsque l'on encode une lettre de l'alphabet ou un pourcentage qui n'entre pas dans le range supporté (à savoir entre 0 et 100).

samedi, juin 23 2012

Création d'un Custom WebControl avec du JavaScript embedded

Mon précédent article expliquait comment réaliser simplement un contrôle en prenant un peu la main sur ce qui se faisait en ASP.NET et en simplifiant les propriétés pour coller plus au besoin réel. Nous continuons dans cette direction mais en offrant maintenant à notre contrôle de nouvelles options. Nous ne voulons pas toujours exécuter toutes les validations côté serveur... En effet, certaines sont simples et peuvent s'effectuer sur le poste client. Par exemple ici, un pourcentage est toujours compris entre 0 et 100. Nous devons donc changer notre classe pour lui ajouter un "support" du JavaScript. Ce support permettra d'avoir une classe JavaScript de définie et instanciée pour chaque contrôle de ce type rendu à l'écran. Nous  mettrons le border en rouge si la valeur encodée dans la textbox est incorrecte.

La première étape est d'ajouter l'interface IScriptControl (qui se trouve dans la dll System.Web.Extensions) sur notre classe pourcentage.

[DefaultProperty("Value")]
[ToolboxData("<{0}:PercentageControl runat=server></{0}:PercentageControl>")]
[ToolboxBitmap(typeof(PercentageControl), "Percentage.bmp")]
public class PercentageControl : WebControlIPostBackDataHandlerIScriptControl

Nous devons ensuite enregistrer cette classe dans le ScriptManager comme nous avons dû le faire pour enregistrer celle-ci dans le support du PostBack.

protected override void OnPreRender(System.EventArgs e)
{
    if (!this.DesignMode)
    {
        ScriptManager sm = ScriptManager.GetCurrent(Page);
        if (sm == null)
        {
            throw new HttpException("A ScriptManager control must exist on the page.");
        }
        sm.RegisterScriptControl(this);
    }
    base.OnPreRender(e);
    this.Page.RegisterRequiresPostBack(this);
}
protected override void Render(HtmlTextWriter writer)
{
    if (!this.DesignMode)
    {
        ScriptManager sm = ScriptManager.GetCurrent(Page);
        sm.RegisterScriptDescriptors(this);
    }
    base.Render(writer);
}

Il est temps de passer à l'implémentation de l'interface IScriptControl. Il faut donner le nom de la classe JavaScript liée au contrôle C# ainsi que les propriétés que l'on envoie à la classe JavaScript. Cela se passe dans la méthode GetScriptDescriptors. De l'autre côté, il faut spécifier le chemin pour accéder au fichier JavaScript où se trouve ma (future) classe JavaScript. Cela se passe dans la méthode GetScriptReferences.

public IEnumerable<ScriptDescriptor> GetScriptDescriptors()
{
    ScriptControlDescriptor descriptor = new ScriptControlDescriptor("MyCustomControls.PercentageControl"this.ClientID);
    descriptor.AddProperty("errorCssClass"this.ErrorCssClass);
    return new ScriptDescriptor[] { descriptor };
}
public IEnumerable<ScriptReference> GetScriptReferences()
{
    ScriptReference reference = new ScriptReference();
    if (this.Page != null)
    {
        reference.Path = this.Page.ClientScript.GetWebResourceUrl(
                                        this.GetType(),
                                        "MyCustomControls.PercentageControl.js");
    }
    return new[] { reference };
}

Nous pouvons démarrer notre classe JavaScript. Nous décidons de la placer dans une ressource "embedded" de notre dll. Nous ajoutons notre fichier PercentageControl.js et nous l'enregistrons comme ressource dans notre assembly via cet déclaration ci-dessous.

[assemblyWebResource("MyCustomControls.PercentageControl.js""text/javascript")]

La prochaine étape est l'implémentation de celle-ci.

1) Nous enregistrons notre namespace

Type.registerNamespace('MyCustomControls');


2) Nous allons initialiser les valeurs par défauts etc

MyCustomControls.PercentageControl = function (element) {
    MyCustomControls.PercentageControl.initializeBase(this, [element]);
    this._errorCssClass = null;
};


3) Nous implémentons notre classe protoype avec 2 méthodes .... initialize et dispose

MyCustomControls.PercentageControl.prototype = {
    initialize: function () {
        MyCustomControls.PercentageControl.callBaseMethod(this'initialize');
    },
    dispose: function () {
        $clearHandlers(this.get_element());
        MyCustomControls.PercentageControl.callBaseMethod(this'dispose');
    }
};

4) Nous implémentons les propriétés pour le css d'erreur à appliquer en JavaScript

MyCustomControls.PercentageControl.prototype = {
    initialize: function () {
        MyCustomControls.PercentageControl.callBaseMethod(this'initialize');
    },
    dispose: function () {
        $clearHandlers(this.get_element());
        MyCustomControls.PercentageControl.callBaseMethod(this'dispose');
    },
    set_errorCssClass: function (value) {
        if (this._errorCssClass !== value) {
            this._errorCssClass = value;
            this.raisePropertyChanged('errorCssClass');
        }
    },
    get_errorCssClass: function () {
        return this._errorCssClass;
    }
};

5) Nous donnons explicitement les descripteurs (utile pour la sérialisation JSON)

MyCustomControls.PercentageControl.descriptor = {
    properties: [{ name: 'errorCssClass', type: String}]
};

6) Nous enregistrons la classe et nous informons le framework que c'est chargé.

MyCustomControls.PercentageControl.registerClass('MyCustomControls.PercentageControl', Sys.UI.Control);
if (typeof (Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();

7) Nous souhaitons implémenter notre code "business"

L'idée est de mettre un cadre rouge autour des texbox lorsqu'il y a une erreur. Pour ce faire, nous allons nous abonner à l'event change et ajouter notre css d'erreur en cas de donnée incorrecte. Nous obtenons au final le résultat ci-dessous.

Type.registerNamespace('MyCustomControls');
MyCustomControls.PercentageControl = function (element) {
    MyCustomControls.PercentageControl.initializeBase(this, [element]);
    this._errorCssClass = null;
};
MyCustomControls.PercentageControl.prototype = {
    initialize: function () {
        MyCustomControls.PercentageControl.callBaseMethod(this'initialize');
        this._onchangeHandler = Function.createDelegate(thisthis._onchange);
        $addHandlers(this.get_element(), { 'change'this._onchangeHandler }, this);
    },
    dispose: function () {
        $clearHandlers(this.get_element());
        MyCustomControls.PercentageControl.callBaseMethod(this'dispose');
    },
    _onchange: function (e) {
        var value = parseFloat(e.target.value);
        var isIncorrect = false;
        if (isNaN(value) || !isFinite(value)) {
            isIncorrect = true;
        }
        if (isIncorrect || value > 100 || value < 0) {
            if (this._errorCssClass !== null && this._errorCssClass !== '') {
                this.addCssClass(this._errorCssClass);
            }
        }
        else {
            this.removeCssClass(this._errorCssClass); 
        }
    },
    set_errorCssClass: function (value) {
        if (this._errorCssClass !== value) {
            this._errorCssClass = value;
            this.raisePropertyChanged('errorCssClass');
        }
    },
    get_errorCssClass: function () {
        return this._errorCssClass;
    }
};
MyCustomControls.PercentageControl.descriptor = {
    properties: [{ name: 'errorCssClass', type: String}]
};
MyCustomControls.PercentageControl.registerClass('MyCustomControls.PercentageControl', Sys.UI.Control);
if (typeof (Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();

Il ne reste plus qu'à ajouter ma class css dans ceux-ci

<style type="text/css">
    .errorCss
    {
        border-colorRed;
    }
</style>

et de l'ajouter dans notre code asp.net
 
<div>
    <asp:Button ID="Button1" Text="Click" runat="server" />
    <Custom:PercentageControl ID="PercentageControl1" runat="server" 
        ErrorCssClass="errorCss" Value="100" />
</div>
 
Nous pouvons maintenant l'exécuter et nous voyons que l'on retrouve un cadre rouge si on encode 200, -10 ou encore le "é" dans notre textbox. Vous pouvez retrouver la solution en fichier attaché ci-dessous.

lundi, juin 18 2012

Création d'un Custom WebControl

Depuis que je développe en .NET, j'entend toujours des gens expliquer qu'asp.net génère du code HTML de très mauvaise qualité, etc. Il n'en est rien dans la pratique. En effet, il est tout à fait possible de travailler en ASP.NET et de générer du beau code HTML. J'ai décidé suite à certaines implémentations réalisées il n'y a pas si longtemps ... de réaliser quelques articles sur ce sujet (en effet, nombre d'entre nous ne connait pas toute la puissance d'ASP.NET). Le premier porte ici sur la création d'un contrôle simple affichant une textbox. Côté .NET, nous pouvons récupérer la valeur. J'expliquerai prochainement comment ajouter des validations client JS propres ainsi que la réalisation de contrôles plus complexes.

La première étape est de créer une nouvelle classe héritant de WebControl. Nous décidons de l'appeler PercentageControl.

[ToolboxData("<{0}:PercentageControl runat=server></{0}:PercentageControl>")]
public class PercentageControl : WebControl
{
}

Il s'agit d'un contrôle "pourcentage" qui est censé nous donner un pourcentage. Nous décidons donc de lui rajouter une propriété renvoyant le pourcentage. Nous définissons également, par attribut, que la "default property" est cette propriété. Nous allons également garder en ViewState cette valeur si elle existe.
 
[DefaultProperty("Value")]
[ToolboxData("<{0}:PercentageControl runat=server></{0}:PercentageControl>")]
public
 class PercentageControl : WebControl
{
    [Bindable(true)]
    [Category("Custom Category")]
    [DefaultValue("")]
    [Description("The percentage value.")]
    [Localizable(false)]
    public double? Value
    {
        get
        {
            var value = this.ViewState["Value"];
            if (value != null)
            {
                return (double)value;
            }
            return default(double?);
        }
        set { this.ViewState["Value"] = value; }
    }
}
 
Nous avons maintenant notre propriété mais nous n'avons encore aucun HTML généré... L'idée est donc maintenant d'implémenter le code C# qui après processing affichera du code HTML.
 
protected override HtmlTextWriterTag TagKey
{
  get { return HtmlTextWriterTag.Input; }
}
protected override void AddAttributesToRender(HtmlTextWriter writer)
{
   base.AddAttributesToRender(writer);
   writer.AddAttribute(HtmlTextWriterAttribute.Type, "text");
   writer.AddAttribute(HtmlTextWriterAttribute.Name, this.UniqueID);
   writer.AddAttribute(HtmlTextWriterAttribute.Value, this.Value.HasValue 
                                                      ? this.Value.ToString() 
                                                      : string.Empty);
}
 
Si nous exécutions ce code dans une page HTML, nous verrions donc une textbox. Mais, si nous ajoutions un bouton ou tout autre contrôle provoquant un PostBack, nous verrions que le contrôle serait chaque fois remis à la valeur enregistrée par défaut. Pour garder le contrôle à jour et pour mettre à jour la propriété "Value", il faut implémenter l'interface IPostBackDataHandler et enregistrer notre contrôle comme supportant le "postback". Via ce mécanisme, nous pouvons par exemple lancer un event pour annoncer que notre pourcentage a été modifié lors du postback.
 
protected override void OnPreRender(System.EventArgs e)
{
   base.OnPreRender(e);
   this.Page.RegisterRequiresPostBack(this);
}

#region IPostBackDataHandler Members
public bool LoadPostData(string postDataKey, NameValueCollection postCollection)
{
   var result = postCollection[this.UniqueID];
   if (string.IsNullOrEmpty(result))
   {
       this.Value = default(double?);
       return !this.Value.HasValue;
   }
   double newValue;
   if (double.TryParse(result, out newValue))
   {
      bool hasChanged = this.Value != newValue;
      this.Value = newValue;
      return hasChanged;
   }
   return false;
}

public void RaisePostDataChangedEvent()
{
}
#endregion
 
Nous obtenons un contrôle fonctionnel. Il ne nous reste plus qu'à l'utiliser. Afin d'améliorer un petit peu le look and feel pour nous développeurs, nous pouvons ajouter une icône qui sera visible dans la toolbox de Visual Studio. Nous ajoutons une image en "embedded resource" et nous ajoutons un attribut sur notre classe qui pointe vers ce fichier.
 
[ToolboxBitmap(typeof(PercentageControl), "Percentage.bmp")]
 
 
Voici le fichier final:  
 
[DefaultProperty("Value")]
[ToolboxData("<{0}:PercentageControl runat=server></{0}:PercentageControl>")]
[ToolboxBitmap(typeof(PercentageControl), "Percentage.bmp")]
public class PercentageControl : WebControlIPostBackDataHandler
{
    [Bindable(true)]
    [Category("Custom Category")]
    [DefaultValue("")]
    [Description("The percentage value.")]
    [Localizable(false)]
    public double? Value
    {
        get
        {
            var value = this.ViewState["Value"];
            if (value != null)
            {
                return (double)value;
            }
            return default(double?);
        }
        set { this.ViewState["Value"] = value; }
    }
    protected override HtmlTextWriterTag TagKey
    {
        get { return HtmlTextWriterTag.Input; }
    }
    protected override void AddAttributesToRender(HtmlTextWriter writer)
    {
        base.AddAttributesToRender(writer);
        writer.AddAttribute(HtmlTextWriterAttribute.Type, "text");
        writer.AddAttribute(HtmlTextWriterAttribute.Name, this.UniqueID);
        writer.AddAttribute(HtmlTextWriterAttribute.Value, this.Value.HasValue 
                                                            ? this.Value.ToString() 
                                                            : string.Empty);
    }
    protected override void OnPreRender(System.EventArgs e)
    {
        base.OnPreRender(e);
        this.Page.RegisterRequiresPostBack(this);
    }
    #region IPostBackDataHandler Members
    public bool LoadPostData(string postDataKey, NameValueCollection postCollection)
    {
        var result = postCollection[this.UniqueID];
        if (string.IsNullOrEmpty(result))
        {
            this.Value = default(double?);
            return !this.Value.HasValue;
        }
        double newValue;
        if (double.TryParse(result, out newValue))
        {
            bool hasChanged = this.Value != newValue;
            this.Value = newValue;
            return hasChanged;
        }
        return false;
    }
    public void RaisePostDataChangedEvent()
    {
    }
    #endregion
}
 
Nous pouvons dès à présent le référencer et l'utiliser dans notre code ASP.NET.
 
<div>
    <Custom:PercentageControl runat="server" Value="200" />
    <asp:Button Text="Click" runat="server" />
</div>
 

lundi, décembre 5 2011

Conserver le "tab" courant en cas de postback avec JQuery UI Tabs

Nous souhaitons souvent amener un caractère sexy à notre page... Le framework à la mode est JQuery et il a intégré dans son plugin UI le composant permettant d’afficher une zone sous forme de 'tabs'. Néanmoins, l'utilisation d'ASP.NET amène parfois des problèmes. En effet, le moindre "changement" dans une dropdown en autopostback provoque un rechargement de la page. Evidement, JQuery UI Tabs étant 100% côté client, il est impossible de revenir sur le tab "courant" qui a déclenché le PostBack...

Cependant, nous pouvons facilement ajouter ce type de fonctionnalité dans notre code. Cet article l'explique.

Prenons par exemple une page ASP.NET de base héritant d'une MasterPage.

<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
    <div class="tabs">
        <ul>
            <li><a href="#tabs1">Tab 1</a></li>
            <li><a href="#tabs2">Tab 2</a></li>
        </ul>
        <div id="tabs1">
            Tab 1
            <asp:Button runat="server" Text="Click from tab 1" />
        </div>
        <div id="tabs2">
            Tab 2
            <asp:Button runat="server" Text="Click from tab 2" />
        </div>
    </div>
</asp:Content>


Nous ajoutons les script JQuery et JQuery UI via CDN. Nous ajoutons également la mise en place du mécanisme de tabs sous JQuery UI, à savoir l'utilisation de la méthode tabs() sur notre div.

<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
    <script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.1.min.js"></script>
    <script type="text/javascript" src="http://ajax.microsoft.com/ajax/jquery.ui/1.8.5/jquery-ui.js"></script>
    <link type="text/css" rel="stylesheet" href="http://ajax.microsoft.com/ajax/jquery.ui/1.8.5/themes/redmond/jquery-ui.css" />
    <script type="text/javascript" language="javascript">
        $(function () {
            $("div.tabs").tabs();
         });
    </script>
</asp:Content>

Notre page s'affiche et les div sont correctement rendus avec des tabs. Nous devons maintenant ajouter la possibilité de retenir le tab courant. Nous allons donc ajouter dans notre page un input field de type hidden:

<asp:HiddenField runat="server" ID="currentTab" />

Nous allons également modifier notre code javascript pour retenir le tab courant:

<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
    <script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.1.min.js"></script>
    <script type="text/javascript" src="http://ajax.microsoft.com/ajax/jquery.ui/1.8.5/jquery-ui.js"></script>
    <link type="text/css" rel="stylesheet" href="http://ajax.microsoft.com/ajax/jquery.ui/1.8.5/themes/redmond/jquery-ui.css" />
    <script type="text/javascript" language="javascript">
        $(function () {
            $("div.tabs").tabs();
            var currentTabId = $("#<%= currentTab.ClientID %>");
            $("div.tabs").tabs('select', currentTabId.val());
            $("div.tabs").bind('tabsselect'function (event, ui) {
                currentTabId.val(ui.panel.id);
            });
        });
    </script>
</asp:Content>

A partir de ce moment, nous avons une page qui s'affiche sur le bon tab en cas de postback. Nous pouvons donc accéder en C# à l'information via le hiddenfield.Value ou via JS.

jeudi, novembre 17 2011

Ajout de la persistence SQL Compact dans un projet Windows Phone 7

Je continue ma série d'articles sur la création d'une application Windows Phone en parlant de la persistence .. Une application moderne demande souvent de pouvoir garder des informations en mémoire. Une base de données SQL est très souvent utilisée pour obtenir des résultats correspondant à notre recherche ... Nous voulons ici enregistrer tous nos pointages afin de pouvoir effectuer des recherches dans le futur ainsi que des décomptes.

1) Création de la couche Manager de persistence

La première étape est de créer une classe: public static class DataPersistenceManager { }
Nous pouvons ensuite créer notre code définissant notre table:

    [Table(Name = "Items")]
    private class Item
    {
        [Column(IsPrimaryKey = true, IsDbGenerated = true, DbType = "INT NOT NULL Identity",
                CanBeNull = false, AutoSync = AutoSync.OnInsert)]
        public int ID { getset; }
        [Column(CanBeNull = false)]
        public DateTime StartTime { getset; }
        [Column]
        public DateTime? StopTime { getset; }
    }
Il faut ensuite pouvoir enregistrer nos données de notre table "Items" dans notre base de données sur notre Windows Phone 7 Mango. Cela passe par deux étapes:

La première est la création de notre classe DataContext (a noter qu'ici on enregistre cette base dans notre IsolatedStorage):

    private class TimerDataContext : DataContext
    {
        private const string DBConnectionString = "Data Source=isostore:/MyTimerDb.sdf";
        public TimerDataContext() : base(DBConnectionString) { }
        public Table<Item> TimerItems;
    }
La seconde est l'utilisation de celle-ci pour créer ou récupérer une instance de la base de données:

    private static TimerDataContext GetTimerDataContext()
    {
        TimerDataContext context = new TimerDataContext();
        if (!context.DatabaseExists())
            context.CreateDatabase();
        return context;
    }
Nous pouvons ensuite créer deux méthodes utilisées dans l'application. En effet, nous avons besoin d'un Get et d'un InsertOrUpdate.

public static class DataPersistenceManager
{
    public static IEnumerable<TimerItem> GetItems()
    {
        List<TimerItem> items = new List<TimerItem>();
        using (TimerDataContext db = GetTimerDataContext())
        {
            items.AddRange(db.TimerItems.Select(elt =>
              new
 TimerItem { ID = elt.ID, StartTime = elt.StartTime, StopTime = elt.StopTime }));
        }
        return items;
    }
    public static void SaveOrUpdate(TimerItem item)
    {
        using (TimerDataContext db = GetTimerDataContext())
        {
            if (!item.ID.HasValue)
            {
                var newItem = new Item() { StartTime = item.StartTime, StopTime = item.StopTime };
                db.TimerItems.InsertOnSubmit(newItem);    
                db.SubmitChanges();
                item.ID = newItem.ID;
            }
            else
            {
                var itemSaved = db.TimerItems.First(elt => elt.ID == item.ID);
                itemSaved.StartTime = item.StartTime;
                itemSaved.StopTime = item.StopTime;
                db.SubmitChanges();
            }
        }
    }
}

2) Utilisation de notre DB Manager dans notre application

Il faut donc modifier notre classe ViewModel. Celle-ci au chargera à son instanciation les items existants ... Lorsque l'on exécutera notre action, nous enregistrerons notre item dans la DB (le code original se trouve dans l'article lié a MVVM sous Windows Phone 7)

public class MainViewModel : ViewModelBase
{
    private ObservableCollection<TimerItem> items = new ObservableCollection<TimerItem>();
    public ObservableCollection<TimerItem> Items { get { return this.items; } }
    public MainViewModel()
    {
        this.Execute = new RelayCommand(ExecuteAction);
        foreach (var elt in DataPersistenceManager.GetItems())
            this.items.Add(elt);
        if (this.items.Count > 0)
            this.currentItem = this.items[this.items.Count - 1];
    }
    public ICommand Execute { getprivate set; }
    private void ExecuteAction()
    {
        if (this.currentItem == null)
        {
            // no value
            this.currentItem = new TimerItem { StartTime = DateTime.Now };
            this.items.Add(this.currentItem);
        }
        else if (!this.currentItem.StopTime.HasValue)
        {
            this.currentItem.StopTime = DateTime.Now;
        }
        else
        {
            this.currentItem = new TimerItem { StartTime = DateTime.Now };
            this.items.Add(this.currentItem);
        }
        DataPersistenceManager.SaveOrUpdate(this.currentItem);
    }
    private TimerItem currentItem;
}

Ces quelques ajustements nous permettent d'avoir une application qui gardera en mémoire tous nos pointages. Nous pouvons maintenant passer à la phase d'avertissement lorsque nous avons dépassé notre nombre d'heures. Nous verrons ce point dans un de mes prochains articles.

mardi, octobre 18 2011

MVVM Light sous Windows Phone 7

Développant depuis quelques mois sous Windows Phone 7,  je me suis lancé dans un projet « fou ». Celui-ci va me mener à la réalisation d’une application de a à Z expliquée sur mon blog.  Je me repose donc sur des technologies Microsoft et sur le SDK RTW de Windows Phone 7 Mango pour Visual Studio 2010.
Nous démarrons donc cette aventure avec un post expliquant la structure « vide » ainsi que l’architecture utilisée pour mener à bien la mission.

1) Choix d'utilisation du pattern MVVM

Nous allons utiliser le pattern MVVM pour réaliser cette application. L’objectif de ce pattern (inventé par John Gossman) est de simplifier le code « behind » des vues et de permettre de tester son code plus facilement. Nous pouvons pousser plus loin en couplant ce pattern avec le pattern IoC pour totalement découper certaines couches mais, dans cette session ci, nous nous limiterons à l’utilisation de MVVM. Vous pouvez trouver plus d’informations sur MVVM via ce lien.

2) Enregistrement du "Locator" dans l'application

La première des manipulations est de créer puis d'enregistrer en ressource notre classe "Locator". Notre classe est en réalité une facade. Dans notre projet, nous n'avons qu'un seul "view model".

public class ViewModelLocator
{
    private static MainViewModel _main;
    /// <summary>
    /// Initializes a new instance of the ViewModelLocator class.
    /// </summary>
    public ViewModelLocator()
    {
        _main = new MainViewModel();
    }
    /// <summary>
    /// Gets the Main property which defines the main viewmodel.
    /// </summary>
    public MainViewModel Main
    {
        get { return _main; }
    }
}

Nous pouvons enregistrer dans notre XAML notre class en tant que ressource. Nous pourrons par après accéder à notre classe via une clé depuis n'importe quel écran.

<?xml version="1.0" encoding="utf-8"?>
<Application x:Class="WorkCountdown7.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone" xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="clr-namespace:WorkCountdown7.ViewModel" mc:Ignorable="d">
  <!--Application Resources-->
  <Application.Resources>
    <vm:ViewModelLocator x:Key="Locator" d:IsDataSource="True" />
  </Application.Resources>
  <Application.ApplicationLifetimeObjects>
    <!--Required object that handles lifetime events for the application-->
    <shell:PhoneApplicationService Launching="Application_Launching" Closing="Application_Closing" Activated="Application_Activated" Deactivated="Application_Deactivated" />
  </Application.ApplicationLifetimeObjects>
</Application>

3) Implémentation du ViewModel

Nous définissons notre logique business dans cette classe. Notre but est donc d'avoir un bouton permettant de démarrer / arrêter un "chronomètre". Nous devons donc diffuser une API avec une collection d'items ainsi qu'un ICommand permettant de lancer l'action. Nous ne définissons pas de droits sur le bouton(Ceci aurait pu se faire via la classe RelayCommand ...). Nous implémentons notre logique business dans la méthode exécutée lors d'une action "Execute". Cela suffit pour avoir une classe testable à souhait. Nous pouvons passer à l'implémentation de l'interface.

public class MainViewModel : ViewModelBase
{
    private ObservableCollection<TimerItem> items = new ObservableCollection<TimerItem>();
    public ObservableCollection<TimerItem> Items { get { return this.items; } }
    public MainViewModel()
    {
        this.Execute = new RelayCommand(ExecuteAction);
    }
    public ICommand Execute { getprivate set; }
    private void ExecuteAction()
    {
        if (this.currentItem == null)
        {
            // no value
            this.currentItem = new TimerItem { StartTime = DateTime.Now };
            this.items.Add(this.currentItem);
        }
        else if (!this.currentItem.StopTime.HasValue)
        {
            this.currentItem.StopTime = DateTime.Now;
        }
        else
        {
            this.currentItem = new TimerItem { StartTime = DateTime.Now };
            this.items.Add(this.currentItem);
        }
    }
    private TimerItem currentItem;
}

4) Implémentation de l'interface

Nous allons réutiliser notre Locator (expliqué en point 2) en l'enregistrant en tant que DataContext de notre UI.

DataContext="{Binding Source={StaticResource Locator},  Path=Main}"

Nous pouvons par après utiliser un contrôle pivot pour avoir deux "onglets". Le premier contiendra un bouton. Le second contiendra l'historique des actions.

<controls:Pivot>
    <controls:Pivot.Title>
        <TextBlock Text="Hello World !"/>
    </controls:Pivot.Title>
    <!--Pivot item one-->
    <controls:PivotItem Header="Timer">
        <local:Timer/>
    </controls:PivotItem>
    <!--Pivot item two-->
    <controls:PivotItem Header="History">
        <local:History/>
    </controls:PivotItem>
</controls:Pivot>

Dans notre premier onglet, nous avons un bouton pour démarrer/arrêter notre "compteur". Nous pouvons simplement lier notre bouton sur un Command de notre ViewModel ... en l'occurence ici notre Execute.

<Button Content="Button" Command="{Binding Execute}" Height="72" HorizontalAlignment="Left" Name="button1" VerticalAlignment="Top" Width="160" />

Dans notre second onglet, nous décidons que nos items rempliront la listbox.

<ListBox DataContext="{Binding Path=Items}" ItemsSource="{Binding}">
</ListBox>

4) Ensuite ? 

Nous pouvons exécuter notre code. Celui-ci fonctionnera parfaitement ... Il est testable et surtout il est possible sans problème de repenser l'interface graphique via Expression Blend sans toucher à une ligne de notre code business. Vous pouvez télécharger le code complet via ce lien. Le prochain post abordera la persistence des données dans une base SQL Compact afin d'historiser / requêter simplement nos "timers".

- page 1 de 19