Fusionner plusieurs fichiers XPS

by Gilles DEHAIS 8. octobre 2011 00:23

Les fichiers XPS sont un peu l'équivalent du PDF mais version Microsoft. En effet, ce type de fichier permet l'impression d'un document avec un rendu équivalent que ça soit sur différents ordinateurs et/ou imprimantes. Il permet aussi de bloquer l'édition puisqu'un fichier XPS n'est pas éditable. Il s'agit d'une "image" des pages du document d'origine.

Cependant, il se peut que vous ayez à fusionner plusieurs fichiers XPS pour obtenir un seul fichier final. Nous allons voir que cela est possible avec quelques lignes de codes et grâce à l'API Document qui contient l'API XPS.

 

Tout d'abord, il faut créer notre fichier final qui sera la fusion de nos différents fichiers XPS.

 

dstPackage = Package.Open(dstMergedXpsFileName, FileMode.Create);
dstMergedXps = new XpsDocument(dstPackage);
FixedDocumentSequence dstDocumentSequence = new FixedDocumentSequence();
xpsWriter = XpsDocument.CreateXpsDocumentWriter(dstMergedXps);

 

On utilise l'objet Package pour créer notre fichier sur le disque dur puis nous créons notre document XPS qui contiendra la séquence des documents (nos différentes pages). Un DocumentWriter est aussi nécessaire pour pouvoir remplir notre fichier XPS et l'enregistrer.

Maintenant que notre document final est prêt à recevoir les pages des documents XPS que l'on souhaite fusionner, il suffit d'ouvrir chacun de ces documents, en récupérer les pages (leurs références) et les ajouter dans notre document final les uns après les autres.

 

srcXps = new XpsDocument(srcXpsFileName, FileAccess.Read);
FixedDocumentSequence srcDocumentSequence = srcXps.GetFixedDocumentSequence();
DocumentPaginator DocPager = srcDocumentSequence.DocumentPaginator;
printTickets.Add(srcXps.FixedDocumentSequenceReader.PrintTicket);
foreach (DocumentReference srcReference in srcDocumentSequence.References)
{
     DocumentReference dstReference = new DocumentReference();
     (dstReference as IUriContext).BaseUri = (srcReference as IUriContext).BaseUri;
     dstReference.Source = srcReference.Source;
     dstDocumentSequence.References.Add(dstReference);
}

 

Comme décrit dans le code ci-dessus, on ouvre notre document, on récupère sa séquence de pages (FixedDocumentSequence) puis pour chaque page, on ajoute une référence (DocumentReference) à notre fichier final, son adresse (sa localisation dans le fichier XPS qui n'est en faite qu'un fichier ZIP) et sa source qui est notre page en elle même.

On peut voir aussi la notion de "PrintTicket". Les "PrintTicket" dans les fichiers XPS permettent de définir si la page est en mode portrait ou paysage. Ainsi, l'imprimante pourra imprimer certaines pages en mode paysage tandis que d'autres le seront en mode portrait.

Enfin, il ne nous reste plus qu'à sauvegarder notre fichier final après avoir fusionner tous nos fichiers. Ceci se fait de la manière suivante :

 

xpsWriter.Write(dstDocumentSequence);
dstMergedXps.Close();
dstPackage.Close();

 

Voilà, notre fichier contient maintenant toutes les pages des fichiers que l'on souhaitait fusionner en un seul. Il reste cependant un petit bémol à la fusion de fichiers XPS. Ainsi, si vous avez une pagination dans chacun de vos fichiers, cette pagination n'est pas modifiée car une page dans un fichier XPS ne peut être modifiée car elle est devenu une "image". Vous pouvez modifier l'extension de vos fichiers XPS et la renommer en ".zip" pour découvrir l'architecture complète d'un fichier XPS.

Je vous joins à cette article le code source.

FusionXPS.zip (30,07 kb)

Tags:

Lancer un processus asynchrone en Asp.Net

by Gilles DEHAIS 11. juillet 2011 20:46

Bonjour,

Nous allons voir comment lancer un processus de façon asynchrone en ASP.Net. En effet, vous vous êtes surement retrouver devant le cas d'une page qui doit lancer un processus relativement long (plusieurs minutes) et qui bloque alors la page Web en attendant que ce processus finisse et que le serveur réponde que l'opération s'est déroulée avec succès ou non.

Or, lorsque l'on clique sur le bouton qui lance ce processus sur le serveur, la page est comme "frisée" et les animations permettant d'indiquer à l'utilisateur d'attendre sont elles aussi figées. Heureusement, il existe un moyen de contourner ce problème : l'asynchrone.

Créer un site Asp.Net vide sous Visual Studio et ajoutez-y un autre page Web que l'on nommera "Default2.aspx". Nous allons maintenant éditer la page "Default.aspx" pour y ajouter du code javascript et nos éléments Web.

 

<title> Untitled Page </title> 
<script type="text/javascript"> 
 
    function ClientCallbackFunction(arg, ctx) {
        if (arg != "") {
            window.location = "Default2.aspx";
        }
    } 
</script>  
</head>


<body> 
 
<form id="form1" runat="server"> 
<div>
Async Callback
<br />
<asp:TextBox ID="tbxLongProcessArgs" runat="server" />
<br />
<input type="button" value="Start" id="bttAsync"
       onclick="javascript:document.all.Message.innerText='Process started';return DoTheCallback(document.all.tbxLongProcessArgs.value,1);" />
<br />
<span id="Message"></span>
</div>
</form>
</body>

 

Comme vous pouvez le constater, le bouton affiche le message 'Process started' et lance notre processus en mode asynchrone en lui donnant un argument lorsque l'utilisateur cliquera dessus. La fonction JavaScript 'ClientCallbackFunction' sera appelée lorsque notre processus sera terminé et permettra de passer sur notre 2ème page Web.  Maintenant nous allons éditer notre page Default2.aspx

 

<body>
    <form id="form1" runat="server">
    <div>
        <asp:TextBox ID="txtDesc" runat="server" Width="250px" TextMode="MultiLine"></asp:TextBox>
    </div>
    </form>
</body>

 

Cette page va nous permettre d'afficher le résultat de notre processus. Il ne nous reste plus qu'à ajouter le code behind qui lancera notre processus coûteux en temps en mode asynchrone

 

    protected void Page_Load(object sender, EventArgs e)
    {
        string js = Page.ClientScript.GetCallbackEventReference(this, "arg",
        "ClientCallbackFunction", "ctx", true);

        System.Text.StringBuilder sb = new System.Text.StringBuilder();
        sb.Append("function DoTheCallback(arg, ctx) {");
        sb.Append(js);
        sb.Append("}");
        Page.ClientScript.RegisterClientScriptBlock(this.GetType(),
        "callbackkey", sb.ToString(), true);
    }

Comme nous pouvons le voir, nous ajoutons à la page 'Default.aspx.cs' un évènement de type CallBack qui appelera notre fonction javascript 'ClientCallbackFunction' de notre page Default.aspx et ceci de manière totalement asynchrone. Cette évènement sera créé lorsque l'utilisateur cliquera sur le bouton 'Start' de notre page.

Maintenant, pour que la magie de l'asynchrone opère, il va falloir utiliser l'interface 'ICallbackEventHandler' que nous implémentons aussi dans notre code behind de la page Default.aspx.

 

    #region ICallbackEventHandler Members

    void ICallbackEventHandler.RaiseCallbackEvent(string eventArgument)
    {
        this._eventArgument = DoLongProcess(eventArgument);
    }

    string ICallbackEventHandler.GetCallbackResult()
    {
        if (!string.IsNullOrEmpty(this._eventArgument))
            Session["errorMessageImport_P"] = this._eventArgument;
        return this._eventArgument;
    }

    #endregion

 

Lorsque l'utilisateur clique sur le bouton 'Start', notre évènement asynchrone est créé et la méthode RaiseCallbackEvent est lancée. Nous avons aussi la possibilité de lui envoyer des paramètres. C'est cette méthode qui va lancer notre processus côuteux en temps. Une fois le processus fini, la méthode GetCallbackResult est lancée et appelle la méthode javascript 'ClientCallbackFunction' de notre page. Vous pouvez utiliser des variables de Session si vous désirez sauvegarder en mémoire sur le serveur le résultat du processus.

Ainsi, une fois notre processus terminé, la page 'Default2.aspx' est appelée par le client et la réponse retournée par notre processus est affichée à l'utilisateur. Vous pouvez remarquer que lorsque vous cliquez sur le bouton 'Start', la page n'est pas figé. Elle peut donc afficher un gif animé d'attente utilisateur par exemple. 

WebSite_Async.zip (5,03 kb)

Tags:

Développement

Le pattern MVVM en WPF

by Gilles DEHAIS 24. février 2011 20:35

Grâce à cet article, nous allons voir comment lier nos objets graphiques (UI) avec notre code métier en WPF en s’appuyant le modèle MVVM (Model View ViewModel). Commençons par créer une nouvelle application WPF via Visual Studio. Ouvrez le fichier nommé “Window1.xaml”. Nous allons créer notre petite interface graphique avec quelques éléments graphiques dans une simple grille.

<Window x:Class="WpfMVVM.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Binding in WPF" Height="300" Width="300">
    <Grid Margin="5">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="0,3*" />
            <ColumnDefinition Width="0,7*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="100" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <TextBlock Grid.Column="0" Grid.Row="0" Text="First name :" />
        <TextBlock Grid.Column="0" Grid.Row="1" Text="Last name :" />
        <TextBox Grid.Column="1" Grid.Row="0" Text="{Binding Path=Client.FirstName,
UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Margin="0,0,0,10" /> <TextBox Grid.Column="1" Grid.Row="1" Text="{Binding Path=Client.LastName,
UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Margin="0,0,0,10" /> <TextBlock Grid.Column="0" Grid.Row="3" Text="My client is :" /> <TextBlock Grid.Column="1" Grid.Row="3" Text="{Binding Path=Client.FullName,
Mode=OneWay}" /> </Grid> </Window>

Comme vous pouvez le voir, nous avons disposé 3 textbox. Chacune de ces textbox ont un champ “Text” que nous avons bindé à une propriété de notre futur objet Client. Ainsi, nous pourrons renseigner le prénom, le nom et récupérer le nom complet du client dans notre interface graphique.

De plus, nous utilisons l’option “UpdateSourceTrigger” pour mettre à jour nos propriétés dès que l’utilisateur modifie un de ces champs. Sans cette option, le champ est mis à jour uniquement lorsque l’utilisateur quitte le champ modifié. Le mode “TwoWay” permet de lier dans les deux sens notre objet graphique, soit le get et le set de notre propriété. En mode “OneWay”, seul le get de notre propriété est utilisé et donc notre propriété n’est pas modifiable via l’interface graphique.

Nous allons maintenant créer notre objet Client.

public class Client : ViewModelBase
{
    private string _fistName;
    public string FirstName
    {
        get
        {
            return _fistName;
        }
        set
        {
            _fistName = value;
            RaisePropertyChanged("FirstName");
            RaisePropertyChanged("FullName");
        }
    }

    private string _lastName;
    public string LastName
    {
        get
        {
            return _lastName;
        }
        set
        {
            _lastName = value;
            RaisePropertyChanged("LastName");
            RaisePropertyChanged("FullName");
        }
    }

    public string FullName
    {
        get
        {
            return _fistName + " " + _lastName;
        }
    }

    public Client()
    { }
}

Nous avons donc défini nos 3 propriétés liées (bindées) à l’interface graphique. Concentrons nous sur la méthode “RaisePropertyChanged”. En effet, pour alerter notre UI que l’une de nos propriétés à été modifiée, il faut utiliser l’interface “INotifyPropertyChanged”. Ainsi, si notre code métier modifie le prénom de notre client, il faut avertir la UI de rafraichir notre champ “prénom” mais aussi le champ “Nom complet”. On implémente donc notre classe Client par une classe “ViewModelBase”.

public class ViewModelBase : INotifyPropertyChanged
    {
        #region INotifyPropertyChanged Membres
        /// <summary>
        /// Basic implementation of INotifyPropertyChanged
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Raises PropertyChanged event
        /// </summary>
        /// <param name="propertyName">name of the property which has changed</param>
        protected virtual void RaisePropertyChanged(string propertyName)
        {
            VerifyPropertyName(propertyName);
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        protected void VerifyPropertyName(string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName)) return;
            // Verify that the property name matches a real,  
            // public, instance property on this object.
            if (TypeDescriptor.GetProperties(this)[propertyName] == null)
            {
                Debug.Fail(string.Format("Invalid property name '{0}' in" +
"OnPropertyChanged call"
, propertyName)); } } #endregion }

Cette classe “ViewModelBase” implémente INotifyPropertyChanged. Je n’irai pas plus loin dans l’explication d’INotifyPropertyChanged car cela n’est pas le but de cet article, mais sachez que ceci permet d’alerter notre UI que nos objets métiers ont été modifiés.

Il nous reste encore une dernière classe à créer, celle qui regroupe les objets qui doivent être affichés dans la UI. Elle contient donc notre objet Client et le constructeur qui permet la création d’un client que l’on affichera dans notre UI.

public class ClientViewModel
    {
        public Client Client
        {
            get;
            set;
        }

        public ClientViewModel()
        {
            Client = new Client() { FirstName = "Bill", LastName = "Smith" };
        }
    }

Pour finir, aller dans le code-behind de la page WPF de notre application. Il faut ici définir ce qu’on appelle le “DataContext” de notre page. Ainsi, on lie notre classe “ClientViewModel” avec la page graphique de notre application. Lorsque la page va être créée, son contexte va être rattaché à notre classe, qui contient tous les objets métiers que l’on souhaite afficher sur notre page.

public partial class Window1 : Window
    {
        ClientViewModel _viewModel;

        public Window1()
        {
            InitializeComponent();
            _viewModel = new ClientViewModel();
            this.DataContext = _viewModel;
        }
    }

Appuyer sur F5 dans Visual Studio. La fenêtre WPF apparait et vous pouvez voir que nos textbox sont automatiquement remplies par le prénom, nom et nom complet du client que nous avons créé dans notre ViewModel (ClientViewModel). Si vous redéfinissez le prénom et/ou le nom du client dans l’interface graphique, notre objet métier client est alors automatiquement mis à jour et son nom complet est rafraichit dans l’interface automatiquement.

Ainsi, grâce au pattern MVVM que nous venons de voir, vous n’avez plus à vous préoccuper de la mis à jour de vos objets métiers par rapport à votre interface graphique et vice-versa. Cela rend votre application beaucoup plus flexible et plus robuste.

WpfMVVM.zip (57,84 kb)

Tags: , ,

Développement

VSTO : Utilisation du ruban Office sous Excel

by Gilles DEHAIS 22. février 2011 02:09

Le ruban d’action office se présente sous la forme d’un panneau latéral, par défaut à droite, et qui permet de rajouter des actions personnalisées. Ainsi, nous pouvons rajouter à Excel, Word… nos propres UI. Ainsi, des actions répétitives peuvent être codées en VSTO et permettre à l’utilisateur de cliquer sur un bouton et d’insérer dans une feuille Excel des données provenant d’une base de données SQL, par exemple.

Le problème ici, se situe dans le fait que l’utilisateur peut ouvrir plusieurs Excel, plusieurs classeurs Excel contenant plusieurs feuilles. Or, dans certains cas, l’affichage de notre UI dans le panneau d’actions doit différer suivant le fichier Excel ouvert ou la feuille Excel en cours d’édition.

Pour cela, il va falloir s’appuyer sur le nom de la feuille Excel. Or ce nom peut être changer par l’utilisateur. Il faut donc récupérer son nom de code, qui n’est accessible que si le VBA est exécutable sous Excel. Voici la méthode à suivre pour activer ce code VBA, récupérer le nom de code de la feuille et afficher la bonne UI suivant la feuille active dans le classeur Excel.

Tout d’abord, il faut commencer par afficher le panneau d’actions et lui lier un User Control qui hébergera notre UI.

   1:  /// <summary>
   2:  /// Create the task panel
   3:  /// </summary>
   4:  public void CreateTaskPanel()
   5:  {
   6:       // Création de notre panneau UI
   7:       _taskPaneControl = new TaskPaneControl();
   8:       // Assigne le panneau a notre panneau d'action Office, ici celui d'Excel
   9:       _customTaskPane = Globals.ThisAddIn.CustomTaskPanes.Add(_taskPaneControl,
                                                                    "My panel control");
  10:       _customTaskPane.Width = 350;
  11:       _customTaskPane.Visible = true;
  12:       //Récupère la version d'Excel
  13:       string version =
                     Globals.ThisAddIn.Application.GetType().InvokeMember("Version",
                     System.Reflection.BindingFlags.GetProperty, null,
                     Globals.ThisAddIn.Application, null,
                     new CultureInfo("en-US")).ToString();
  14:       SetVBTrusting(version);
  15:  }

Nous allons maintenant voir comment rendre exécutable du VBA sous Excel. Pour cela, il va falloir attaquer le registre de Windows pour créer une clé si elle n’existe pas et lui passer une valeur “1”. Voici comment faire :

   1:  /// <summary>
   2:  /// Customise le registre Windows pour permettre l'exécution VBA sous Office.
   3:  /// </summary>
   4:  /// <param name="excelVersion">Excel version</param>
   5:  public static void SetVBTrusting(string excelVersion)
   6:  {
   7:       if (string.IsNullOrEmpty(excelVersion))
                   throw new ArgumentNullException("excelVersion");
   8:       string keyPath = string.Format(
@"Software\Microsoft\Office\{0}\Excel\Security", excelVersion);
   9:       RegistryKey key = null;
  10:       try
  11:       {
  12:             key = Registry.CurrentUser.OpenSubKey(keyPath, true);
  13:             if (key != null)
  14:                 key.SetValue("AccessVBOM", 1);
  15:             else
  16:                 throw new Exception("La clé VBOM ne peut être trouvé.");
  17:        }
  18:        catch (SecurityException e)
  19:        {
  20:             throw new Exception("La clé VBOM ne peut être mis a jour.", e);
  21:        }
  22:        finally
  23:        {
  24:            if (key != null)
  25:            {
  26:                  key.Close();
  27:            }
  28:        }
  29:  }

La ligne 12 permet de tenter de récupérer la clé “VBOM” sur laquelle se base Excel pour rendre exécutable du code VBA, puis la ligne 14 rend cette fonction utilisable.

Enfin, nous avons rajouter dans notre UI un simple label qui va nous permettre de rendre visible le nom de code de la feuille Excel en cours d’exécution. A chaque changement de feuille, il faut donc rafraichir ce label. Voici comment procédé :

   1:  /// <summary>
   2:  /// Rafraichit notre UI dans le panneau de control
   3:  /// </summary>
   4:  /// <param name="currentSheet">Feuille Excel en cours d édition</param>
   5:  private void RefreshTaskPanel(Excel.Worksheet currentSheet)
   6:  {
   7:        if (currentSheet != null)
   8:        {
   9:             string codeName = 
                  currentSheet.Application.ActiveWorkbook.VBProject.VBComponents.Item
                  (currentSheet.CodeName).Name;
  10:             _taskPaneControl.lblSheetName.Text = codeName;
  11:        }
  12:   }

Bien sûr, toutes ces méthodes doivent être appelées dans les bons évènements d’Excel tel que l’évènement d’activation de la feuille Excel.

La capture d’écran suivante montre bien que notre panneau d’action appelé “My panel control”, qui contient notre User Control qui comprend lui même notre label, affiche bien le nom de code de la première feuille, qui est “Feuil1”, alors que l’utilisateur à renommer cette feuille “Tableau1”.

ResultatFinal

Vous pouvez donc maintenant créer une UI différentes suivant la feuille sélectionnée par l’utilisateur, rendant ainsi votre AddIn plus flexible aux exigences de vos clients. Vous trouverez, joins à cet article, tout le code source.

VSTOTaskPane.zip (85,12 kb)

Tags: ,

Développement

VSTO : Les formules et les formats sous Office

by Gilles DEHAIS 20. février 2011 18:23

Lors de l’utilisation de VSTO (Visual Studio Tools for Office), le développeur ne sait jamais à l’avance sur quelle langue l’Office 2007, ou plus récemment 2010, est utilisée par l’utilisateur final. Cela peut entrainer des bugs ou des formats erronés lors de l’utilisation du complément Office que vous avez développé.

Pour éviter tout problème, il est crucial de lier sont code à une culture précise et neutre pour chaque office disponible sur le marché. De base, la culture neutre est la culture “en-US”. Nous allons donc l’utiliser pour rentrer nos formules sous Excel, par exemple.

   1:  //Récupération de la culture utilisateur.
   2:  CultureInfo currentCulture = Thread.CurrentThread.CurrentCulture;
   3:  try
   4:  {
   5:          //Définition de la culture en-Us pour notre thread Excel
   6:          Thread.CurrentThread.CurrentCulture = new CultureInfo("en-Us");
   7:          Excel.Worksheet sheet = (Excel.Worksheet)Sh;
   8:          sheet.get_Range("A1", "A1").Value2 = "10";
   9:          sheet.get_Range("A2", "A2").Value2 = "3";
  10:          //Création de la formule sur la cellule A3
  11:          sheet.get_Range("A3", "A3").Formula = "=SUM(A1:A2)";
  12:  }
  13:  finally
  14:  {
  15:          //Re-définition de la culture utilisateur pour notre thread Excel
  16:          Thread.CurrentThread.CurrentCulture = currentCulture;
  17:  }

Comme nous pouvons le voir, les lignes 8 et 9 nous permettent de remplir réciproquement les cellules A1 et A2 avec les valeurs 10 et 3. La ligne 11 permet d’associer la cellule A3 à notre formule de somme entre les cellules A1 et A2. La valeur qui s’inscrit alors dans notre cellule A3 est donc 13.

La capture d’écran suivante nous montre bien que, même si notre application est installée sur un Excel français, la somme s’effectue bien et d’ailleurs, la formule affichée n’est pas “=SUM(A1:A2)” mais bien la formule traduite en français, soit “=SOMME(A1:A2)”.

 

SUM_thumb[5]_thumb

 

Ceci va de même pour les formats que l’on souhaite appliqués sur une cellule ou un groupe de cellule, nommé “Range”. En effet, en anglais, le séparateur de milliers est la virgule et le séparateur de décimal est le point. En français, le séparateur de milliers est l’espace et le séparateur de décimal est la virgule. Si vous ne faites pas comme précédemment, vous aurez des formats aberrent suivant la langue de l’Office installé sur le poste utilisateur.

   1:  //Récupération de la culture utilisateur.
   2:  CultureInfo currentCulture = Thread.CurrentThread.CurrentCulture;
   3:  try
   4:  {
   5:        //Définition de la culture en-Us pour notre thread Excel
   6:        Thread.CurrentThread.CurrentCulture = new CultureInfo("en-Us");
   7:        Excel.Worksheet sheet = (Excel.Worksheet)Sh;
   8:        Excel.Range range = sheet.get_Range("A1", "A3");
   9:        //Création du format avec 3 chiffres après le séparateur de décimal
  10:        range.NumberFormat = "###,###,##0.000";
  11:  }
  12:  finally
  13:  {
  14:        //Re-définition de la culture utilisateur pour notre thread Excel
  15:        Thread.CurrentThread.CurrentCulture = currentCulture;
  16:  }

La ligne 10 permet de définir le format tel qu’il devrait apparaitre pour une version US d’Office car nous nous avons établit la culture d’Excel en “en-US” qui est la version neutre. Ainsi, sur un Excel français, l’affichage sera bien rétablit pour la langue française.

 

FORMAT_thumb[2]_thumb

 

Vous savez maintenant rendre votre complément compatible avec n’importe quelle version d’Office.

Tags:

Développement

Neos-SDI  Neos-SDI