Thursday, May 8, 2008

Sharepoint Item Update is a NIGHTMARE!

What a strange statement!

Why?

Actually, using item updates is easy and you get what you want, your item updated. Right.
Let's try an example:
You have to create several items, in several content types, in several folders. Let's assume you want to have a kind of hierarchy like:

  • Folder 1 with a specific content type
    • Items with a specific content type inside this folder
    • A new folder structure with a specific content type
      • Items inside this folder

Logically, you use a code like the following for the folder:

SPFolder ArchiveFolder = MonthFolder.SubFolders.Add("Folder 1");
ArchiveFolder.UniqueContentTypeOrder = contentTypes;
ArchiveFolder.Update();

//Process all about the folder item
ArchiveFolder.Item["Title"] = this._PONumber;
ArchiveFolder.Item["PONumber"] = this._PONumber;
ArchiveFolder.Item["ContentTypeId"] = ePOFolder.Id.ToString(); // Fix the content type for the folder
ArchiveFolder.Item.Update();

And the following code for a document:

SPFile fileH = HistoryFolder.Files.Add(this.getUniqueFileName(), this._docSize);
fileH.Update();
//Getthe item and fill it with history data, plus the content type id
SPListItem itemH = fileH.Item;
itemH["Title"] = "TITRE";
itemH["ContentTypeId"] = historyCT.Id.ToString();
itemH.UpdateOverwriteVersion();

But all of this works fine if you have only a small number of items, ie less than let's say 50000 records in your list (not only in your container)

If you are above this number of records, things start to be less fancy. One update takes 1 second, then 2 seconds, then 4 seconds, etc... I had the case with more than 120 000 records, and one update could take 1 or 2 minutes! Unbelievable.

After googling and looking at my own experience (see here) I've solved one aspect, uploading documents with meta-data :

First, store your item data into a Hashtable:

Hashtable result = new Hashtable();
result.Add("Title",_PONumber); 
result.Add("HistoryDescription",_historyDescription);
result.Add("HistoryOutcome",_historyOutcome);
result.Add("ContentTypeId", CT_Id);

then use the corresponding overload of SPFiles.Add():

SPFile fileH = HistoryFolder.Files.Add(this.getUniqueFileName(), this._docSize, result);

And that's all! You don't need any update! It's automatically done! Great. It was not so obvious but there is an overload for this method.

Now, what about folders. If you try SPFolders.Add()... you don't see any overload like with SPFiles. So you may think there is no way to use the same technic in order to avoid at least one Update (on item)?

Well, have a look on the bad (un)documented property "Properties" of SPFolder: it is a Hashtable! So you can use the add method to add your items properties, then you only need one update, instead of two. Here is the previous code modifed:

ArchiveFolder = MonthFolder.SubFolders.Add("Folder 1");
ArchiveFolder.UniqueContentTypeOrder = contentTypes;
//Process all about the folder item
ArchiveFolder.Properties.Add("Title", this._PONumber);
ArchiveFolder.Properties.Add("PONumber", this._PONumber);
ArchiveFolder.Properties.Add("ContentTypeId", ePOFolder.Id.ToString()); // Fix the content type for the folder
ArchiveFolder.Update();

Be aware of one thing: in Properties you can only add basic data type, like DateTime, String, Int... but no Double for instance. So be careful if you numeric data.

Summary:

We have now only one update for each folder and no more updates for documents. With my set of 120 000 rows, actually more than 500 000, it takes less than one second to add 2 folders and 12 documents. In one minute now I can process 70 times more documents and folders than before!

Wednesday, April 16, 2008

SharePoint Folder, Sub-Folder and Content Type (part 2)

In the previous post we saw how to make a folder hierarchy with there own content type, their own view and their own "New" menu from the user interface. The interesting part of this method is it is easy to implement with only a few clics. But the bad side of this method is we have to do this Folder/New Button implementation everytime we create a folder.

One answer would be to make this implementation automatically each time we create a folder. It is possible with SPItemEventReceiver. But let's focus more on how to make this association from the SharePoint object model ?

Let's say I want to have this kind of structure:

  • Year-Month folder
    • Archive Folder (with meta-data)
      • Document (with meta-data)
      • History folder
        • List of history rows (with meta-data)

For example:

  • 2008-04
    • PO12345684
      • 12345678.pdf
      • History as of 2008-04-01
        • history row 1
        • history row 2
        • history row 3
  • 2008-03
    • PO10000684
      • 10000684.pdf
      • History as of 2008-03-01
        • history row 1
        • history row 2
        • history row 3

I have an Archive List where I'll store all these data. Each folder or document will have its own set of meta-data. We will use a Content Type for each of them. These CT are supposed to be implemented and attached to the Archive list.. The archive folder name will be the Purchase Order Number (PONumber). The PO will be copied from another list, Purchase Orders.

Here is the code:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Workflow;

namespace APichot.Services
{

    public class ArchiveData
    {
        //List and Content type names
        //These names will be used to retrieve the content type in the Archive List
        protected string _ArchiveList = "Archive"; //Name of the archive list
        protected string _FolderYM = "Year-Month Folder";
        protected string _FolderCT = "Archive Folder";
        protected string _FolderHCT = "History Folder";
        protected string _HistoryCT = "History Data";
        protected string _purchOrderCT = "Purchase Order Archive";
        protected string _docCT = "Document";
        //Helper for various processes
        protected HelperClass hc;

        protected SPListItem _item;
        protected string _PONumber = string.Empty;
        protected SPList _LstArchive;
        protected string _RecordingDate = string.Empty;

        //Size of the fake document for history
        protected byte[] _docSize = new byte[1];
        //Name of the fake document
        protected Guid _guid = Guid.NewGuid();
        protected int _increment = 0;

        /// <summary>
        /// Get the current item to be moved to the archive list
        /// </summary>
        /// <param name="item">PO Item</param>
        public ArchiveData(SPListItem item) {
            try {
                if (item == null) {
                    throw new ArgumentNullException("item", "This value cannot be null");
                }
                this._item = item;
                this._LstArchive = this._item.Web.Lists[this._ArchiveList]; //this._item.Web.Lists[this._ArchiveList];
                this._PONumber = this._item["PONumber"].ToString();
                if (string.IsNullOrEmpty(this._PONumber)) {
                    throw new Exception("PONumber cannot be null or empty");
                }
            }
            catch (Exception x) {
                Utilities.logMessage("Event Source", "Archive Data", "Constructor", x.ToString(), EventLogEntryType.Error, this._PONumber);
            }
        }
        /// <summary>
        /// Main method to save data to archive
        /// Run the savePO() and saveHistory() methods
        /// </summary>
        /// <param name="wp">Current Workflow properties</param>
        /// <param name="hd">All history data</param>
        /// <returns></returns>
        public bool saveToArchive(HelperClass hc) {
            if (hc == null) {
                throw new ArgumentNullException("hc", "This value cannot be null");
            }
            try {
                this.hc = hc;
                SPFolder ArchiveFolder = this.savePO();
                if (ArchiveFolder != null && ArchiveFolder is SPFolder) {
                    this.saveHistory(ArchiveFolder);
                }
                else {
                    throw new Exception("ArchiveFolder is null or is not an SPFolder");
                }
                return true;
            }
            catch (Exception x) {
                this.hc.logMessage("saveToArchive", x.ToString(), EventLogEntryType.Error);
                return false;
            }
        }
        /// <summary>
        /// Save the PO to the Archive list
        /// </summary>
        /// <returns>Return the archive folder generated to be used by the history folder as a parent folder</returns>
        protected SPFolder savePO() {
            string whereIAm = string.Empty;
            try {
                //Specify the content type to be attached to a folder
                SPContentType FolderYM = this._LstArchive.ContentTypes[this._FolderYM];
                SPContentType Folder = this._LstArchive.ContentTypes[this._FolderCT];
                SPContentType FolderH = this._LstArchive.ContentTypes[this._FolderHCT];
                SPContentType eMailFolder = this._LstArchive.ContentTypes[this._eMailFolderCT];
                SPContentType purchOrder = this._LstArchive.ContentTypes[this._purchOrderCT];
                SPContentType doc = this._LstArchive.ContentTypes[this._docCT];

                List<SPContentType> contentTypes = new List<SPContentType>();

                //Get the item creation date, wich will become the Year/Month folder
                string currentMonthFolder = this.buildMonthFolderName();
                SPFolder MonthFolder;
                //If the folder already exists, get it, else create it
                whereIAm = "Month Folder";
                try {
                    MonthFolder = this._LstArchive.RootFolder.SubFolders[currentMonthFolder];
                }
                catch {
                    this.hc.logMessage("savMonthFolder", "Month folder creation (" + currentMonthFolder + ")", EventLogEntryType.Information);
                    //Create the folder
                    MonthFolder = this._LstArchive.RootFolder.SubFolders.Add(currentMonthFolder);
                    //What goes into the New menu
                    contentTypes.Add(Folder);
                    //Associate the content type to the folder
                    MonthFolder.UniqueContentTypeOrder = contentTypes;
                    //Use the Properties Hashtable to store folder's meta-data 
                    MonthFolder.Properties.Add("Title", currentMonthFolder);
                    MonthFolder.Properties.Add("ContentTypeId", FolderYM.Id.ToString()); // Fix the content type for the folder
                    whereIAm = "Month Folder Update";
                    MonthFolder.Item.Update();
                }

                SPFolder ArchiveFolder;
                whereIAm = "Archive Folder";
                try {
                    //If the folder doesn't exists, then go to Catch
                    ArchiveFolder = MonthFolder.SubFolders[this._PONumber];
                    //Because of international settings (french), the comma is interpreted as a thousand separator by the hastable.
                    //We have to convert it to a period.
                    string poAmount = this._item["POAmount"].ToString().Replace(",", ".");
                    ArchiveFolder.Properties["POAmount"] =  (this._item["POAmount"] == null) ? "0" : poAmount;
                    ArchiveFolder.Properties["VendorName"] =  (this._item["VendorName"] == null) ? string.Empty : this._item["VendorName"].ToString();
                    ArchiveFolder.Properties["POStatus"] =  this._item["POStatus"].ToString();
                    this.hc.logMessage("savArchive Folder", "Archive folder updated", EventLogEntryType.Information);
                }
                catch {
                    //Folder 
                    //Process all about the folder
                    contentTypes = new List<SPContentType>();
                    //What goes into the new menu
                    contentTypes.Add(purchOrder);
                    contentTypes.Add(doc);
                    contentTypes.Add(FolderH);
                    contentTypes.Add(eMailFolder);

                    ArchiveFolder = MonthFolder.SubFolders.Add(this._PONumber);

                    ArchiveFolder.UniqueContentTypeOrder = contentTypes;
                    //Process all about the folder item
                    ArchiveFolder.Properties.Add("Title", this._PONumber);
                    ArchiveFolder.Properties.Add("PONumber", this._PONumber);
                    //Because Properties accepts only int, string and date, and not Double, we must change PO Amount to a string
                    //And transform the comma (decimal) into a period
                    string poAmount = this._item["POAmount"].ToString().Replace(",", ".");
                    ArchiveFolder.Properties.Add("POAmount", (this._item["POAmount"] == null) ? "0" : poAmount);
                    ArchiveFolder.Properties.Add("VendorName", (this._item["VendorName"] == null) ? string.Empty : this._item["VendorName"].ToString());
                    ArchiveFolder.Properties.Add("POStatus", this._item["POStatus"].ToString());
                    ArchiveFolder.Properties.Add("ContentTypeId", Folder.Id.ToString()); // Fix the content type for the folder
                }
                whereIAm = "Archive Folder Update";
                ArchiveFolder.Update();
                //Store a recording date for the History folder name
                this._RecordingDate = DateTime.Now.ToShortDateString().Replace("/", "-") + " " + DateTime.Now.ToShortTimeString().Replace(":", "h");
                //Copy the PO into the new folder
                whereIAm = "Copy file";
                string destUrl = this._item.Web.Site.Url + this._LstArchive.ParentWebUrl + ArchiveFolder.Url + "/" + this.buildName("PO") + ".pdf";
                //Copy the document from Purchase Order list to the Archive list, with meta-data
                this._item.File.CopyTo(destUrl);
                return ArchiveFolder;
            }
            catch (Exception x) {
                this.hc.logMessage("savePO" + whereIAm, x.ToString(), EventLogEntryType.Error);
                return null;
            }
        }
        protected void saveHistory(SPFolder ArchiveFolder) {
            try {
                //Create History Folder
                string hFolderName = this.buildName("History"); 
                SPFolder HistoryFolder = ArchiveFolder.SubFolders.Add(hFolderName);
                List<SPContentType> contentTypes = new List<SPContentType>();
                //CT for the folder
                SPContentType historyFolderCT = this._LstArchive.ContentTypes[this._FolderHCT];
                //CT for the files inside the folder
                SPContentType historyCT = this._LstArchive.ContentTypes[this._HistoryCT];
                //In this folder, we will be able to create only History Data
                contentTypes.Add(historyCT);

                HistoryFolder.UniqueContentTypeOrder = contentTypes;
                HistoryFolder.Properties.Add("Title", hFolderName);
                HistoryFolder.Properties.Add("ContentTypeId", historyFolderCT.Id.ToString());
                HistoryFolder.Update();

                historyFolderCT = null;
                //Store history rows
                this.storeHistoryRows(HistoryFolder, historyCT);
            }
            catch (Exception x) {
                this.hc.logMessage("saveHistory", x.ToString(), EventLogEntryType.Error);
            }
        }
        protected virtual void storeHistoryRows(SPFolder HistoryFolder, SPContentType historyCT) {
            try {
                //Reset the increment number for the UniqueFileName
                this._increment = 0;
                foreach (historyData hdata in this.hc.HistoryDataCollection.ListOfHistoryData) {
                    //Prepare a fake file for storing each history line as in a custom list. Manadatory for a docLib
                    //hData.FillItem methos is:
                    // public Hashtable FillItem(string CT_Id) {
                    //    Hashtable result = new Hashtable();
                    //    result.Add("Title",_PONumber); 
                    //    result.Add("PONumber",_PONumber);
                    //    result.Add("HistoryDescription",_historyDescription);
                    //    result.Add("HistoryOutcome",_historyOutcome);
                    //    result.Add("WFStart",_eventDate);
                    //    result.Add("ApproversName",_approver);
                    //    result.Add("ContentTypeId", CT_Id);
                    //    return result;
                    //}
                    SPFile fileH = HistoryFolder.Files.Add(this.getUniqueFileName(), this._docSize, hdata.FillItem(historyCT.Id.ToString()));
                    fileH = null;
                }
            }
            catch (Exception x) {
                this.hc.logMessage("storeHistoryRows", x.ToString(), EventLogEntryType.Error);
            }
        }

        protected string buildName(string s) {
            return this._PONumber + " " + s.Trim() + " " + this._RecordingDate;
        }
        protected string getUniqueFileName() {
            if (this._increment == 0) {
                this._guid = Guid.NewGuid();
            }
            this._increment++;
            return string.Format(@"{0}-{1:00}.txt", this._guid.ToString(), this._increment);
        }
        protected string buildMonthFolderName() {
            //Get the item creation date, wich will become the Year/Month folder
            DateTime itemCreate = (DateTime) this._item["Created"];
            return string.Format("{0} - {1:00}", itemCreate.Year, itemCreate.Month);
        }
    }
}

As you can see there are some tricky things:

  • For the New button, you have to prepare a List<> with SPContentType and add this list to the folder's UniqueContentTypeOrder property.
  • For setting meta-data to a folder it is actually worth to use the Folder's Properties property (see this post)
  • For inserting a file, it is actually worth to use a Hastable for meta-data associated with SPFile object (see this post again)

Try to avoid as possible the number of updates you make in order to insert data faster. I've explained it in this post

I've spent some time with this, I hope this helps.

Friday, March 21, 2008

Accélerez SharePoint: Purgez les logs !

Lorsqu'on utilise une configuration standard de SQL Server, les fichiers logs ne sont pas paramétrés au mieux. Ceci à des incidences non négligeables sur les performances à venir de SharePoint. En effet, on peut noter que le fichier log de configuration (Config) de SharePoint à tendance à prendre de l'embonpoint au fur et à mesure de son utlisation. Et bien entendu, les logs associés à votre ou vos ContentDB.

Alors si cette gestion des logs n'est pas optimisée, voici une astuce qui permettra de faire un peu de place et de gagner en rapidité. Cette astuce peut de toute façon être utilisée, même si la gestion des logs est bien faite, cela n'empêche pas! Une bonne cure de printemps, c'est le moment.

Comment faire ?

Tout d'abord, il vaut mieux arrêter quelques services, comme le timer et le search :

NET STOP SPTIMERV3

NET STOP OSEARCH

IISRESET

Puis, personnellement, je préfère faire comme si je voulais déplacer la base. Pour éviter que SharePoint se mélange les pinceaux, et inonde l'Event Viewer de messages pas très catholiques, je fais un preparetomove :

stsadm -o preparetomove -contentdb SQLSERVERNAME:CONTENTDBNAME -site http://SITENAME

Ensuite, je vais dans SQL Server Management Studio pour détacher la base CONTENTDBNAME. Assurez-vous que personne n'est connecté.
Puis je retourne dans le file system pour renommer le fichier CONTENTDBNAME.LDF en... ce que vous voulez, mais il ne faut pas l'effacer (pas encore)
Retour à nouveau dans SQL Server Management Studio pour attacher (Attach) la base CONTENTDBNAME, et, dans la fenêtre de confirmation qui apparaît, il faut mettre en surbrillance le nom du fichier log "Not Found" et cliquer sur le bouton Remove.
Ceci fait, cliquez sur Ok et la base est de retour.
Dans les propriétés de la base, vous pouvez maintenant voir que le fichier log est d'environ 1Mo (cela varie selon la configuration de la DB) et vous pouvez ajuster sont incrément ainsi que sa limite maxi.

NET START SPTIMERV3

NET START OSEARCH

Ensuite, retourner sur votre site et... tout doit fonctionner correctement ! Si ce n'est pas le cas, alors vous avez encore la possibilité de renommer à nouveau l'ancien log. Mais si tout fonctionne, alors vous pouvez supprimer les logs renommés.
Pour ma part, j'ai fait cette opération chez un client qui avait deux logs de plus de 50Go pour des DB de 10Go. Et bien, ça a fait de la place, et il y a surtout eu un gain en rapidité en lecture et écriture.

Thursday, March 20, 2008

Sharepoint: Accessing a List with it's Internal Name

Le problème pour accéder aux listes SharePoint est qu'on doit utiliser le nom "public" de la liste, le Title, et non le nom interne:

Si je crée une liste en lui donnant le nom "MaListe"  puis je renomme cette liste en "Ma Liste", alors la liste a deux noms:

  • Le nom interne, MaListe (le premier créé)
  • Le nom public, Ma Liste, suite au renommage.

Si je veux accéder à la liste par programme, je vais utiliser:

SPList list = web.Lists["Ma Liste"];

car la string demandée par la Lists correspond au champs Title de la liste

Mais si on considère que toute liste est un folder, alors on peut contourner le problème en passant par le SPFolder:

public enum ListType { CustomList, Document };
static public SPList GetListName(SPWeb web, ListType type, string InternalName) {
            try {
                SPFolder folder = null;
                switch (type) {
                    case ListType.CustomList:
                        folder = web.Folders["Lists"].SubFolders[InternalName];
                        break;
                    case ListType.Document:
                        folder = web.Folders[InternalName];
                        break;
                    default:
                        break;
                }
                SPList realList = web.Lists[folder.ParentListId];
                return realList;
            }
            catch (Exception x) {
                return null;
            }
        }

Lorsque vous créez une liste, le premier nom que vous donnez devient le nom du folder et donc le nom interne qui ne changera plus. Ensuite, vous pouvez modifier le nom de la liste, mais c'est le champ Title qui est affecté.

J'ai ici mis en avant l'accès aux listes de type "Custom List", qui sont des sous-folder de "Lists", et des listes de type "Document", dont le folder est directement en web root.
Une fois qu'on a le folder, il suffit de récupérer son ParentListId pour obtenir la liste associée.

Thursday, March 13, 2008

SharePoint Folder, Sub-Folder and Content Type (part 1)

Aujourd'hui, je vais décrypter la façon dont on peut travailler avec les Folder et sous-folders dans SharePoint, tout en leur associant des Content Type. J'ai partagé ce blog en deux :

  • Partie 1 : comment faire via l'interface utilisateur
  • Partie 2 : comment utiliser ceci par programme
Voici l'énoncé du problème :
  • Dans une Document Library, je veux avoir des folders qui ont leurs propres meta données (appelons ces folder "Folder Niveau 1")
  • Au niveau root, je ne veux qu'avoir la possiblité de créer des "Folder Niveau 1"
  • Au niveau Root je veux avoir une seule vue qui me montre ces meta données
  • En entrant dans un "Folder Niveau 1", je veux avoir la possibilité de créer des Documents de type contrat et des "Folder Niveau 2" de type Folder
  • Dans un "Folder Niveau 1", je veux avoir une seule vue qui montre les meta données de Document
  • Dans "Folder Niveau 2" je veux avoir une seule vue qui montre les meta données de Document
  • Comment gérer ces folders par programmation (partie 2)

Résolution par l'interface utilisateur

Tout d'abord, il faut créer les content type (CT). Un content type que l'on crée via l'IU va hériter de propriétés d'un parent. Habituellement ce parent est un document, mais rien n'empêche de prendre un Folder comme parent (après tout, c'est aussi un content type)

CTCreation Dans le site settings on trouve la gallerie des CT

CTFolderNiv1

Et on crée le premier CT, Folder Niveau 1. Il faut bien noter que le parent de ce CT est bien le Folder. Je mets le tout dans un group "Folder Group" (pas très inspiré aujourd'hui...)

CTFolderNiv1Cols

Ensuite, on rajoute quelques colonnes. Pour l'exemple, je les ai prises dans les colonnes existantes. Il faut faire de même pour le CT "Folder Niveau 2", mais cette fois je n'ai pas pris de colonnes supplémentaires. Ensuite, il y a le CT pour le document de type contrat

DocContract

Et les colonnes, prises aussi dans les colonnes existantes. DocContractCols Maintenant, il faut retourner dans la Document Library Settings (advanced), autoriser les Content Type, AuthoriseCT Et empêcher la création de Folder standard (si on le souhaite bien sûr) PasFolder Ensuite, il faut choisir les CT qui nous intéressent AjoutCT AllCT Puis, pour n'avoir que "Folder Niveau 1" dans le menu "New", il faut enlever les CT qui ne nous intéressent pas. Attention, cela ne veut pas dire qu'ils ne seront plus accessibles. En fait, il sont toujours attachés à la liste, mais il ne seront simplement pas visible. Vous allez voir qu'on va jouer là-dessus pour filtrer le contenu du menu "New" dans les sous-folder. ChangeDefault ChooseOnlyFn1 Ceci fait, on note dans le menu new qu'il n'y a plus grand chose, hormis la possibilité de créer un "Folder Niveau 1" ChangeMenuFolder2 Avant de créer un "Folder Niveau 1", il faut faire un tour du coté de la vue "All Documents". Cette vue affiche des données standard, et nous sommes plutôt intéressés pour voir les données du CT "Folder Niveau 1". La première étape consiste à modifier cette vue en sélectionnant les bonnes colonnes Alldocview1 puis, petite astuce, il faut aller tout en bas à la rubrique Folders pour dire que cette vue ne sera valable que lorsqu'on est dans le root folder. Cette possibilité est très intéressante car elle va justement nous permettre de sélectionner les vue que l'on souhaite en fonction du niveau de folder dans le quel on est. Alldocview2 Ceci fait, on va créer un "Folder Niveau 1". Faites New / Folder Niveau 1 et vous constatez qu'il y a bien un formulaire d'édition qui nous demande d'entrer les meta données que l'on a associé au folder NewFolderNiv1Edit Et après avoir cliqué sur OK, on a bien dans la liste un folder avec ses meta données NewFolderNiv1Display Maintenant, nous allons modifier les paramètres ce folder pour personnaliser son menu New. Il ne devra permettre que la création des "Folder Niveau 2" et des Contrats. C'est le même principe que sur la liste: dans le menu associé au nom du folder, on trouve l'option "Change New Button Order" ChangeMenuFolder qui va permettre de sélectionner les éléments que l'on souhaite voir s'afficher dans le menu New de ce folder (Folder Niveau 2 et Contrats) ChangeMenuFolder1 Le menu est bien, mais la vue n'est pas bonne ChangeMenuFolder2 En effet, on ne voit pas les meta data spécifiques aux documents contenus dans ce folder. Nous allons créer une vue qui va permettre cela. Cette vue aura un nom, sera une vue par défaut, avec des colonnes appropriées, et surtout, elle sera associée au CT "Folder Niveau 1". Cela devient donc la vue par défaut pour ce folder FolderView1 FolderView2 Et maintenant, en entrant dans ce Folder Niveau 1 la vue précédente apparaîtra. Ce qui fait qu'on maintenant deux vue par défaut, une pour le niveau Root, et une pour le niveau Folder Niveau 1.

Bien sûr, pour le Folder Niveau 2 il faut faire de même, ce qui permet d'avoir une hiérarchie de folder et de vue très intéressante. Mais l'inconvénient majeur de ce système, c'est qu'il faut faire la démarche de sélectionner le "New Button Order" pour chaque nouveau folder créé. Heureusement il existe une méthode par programmation. C'est ce que nous verrons dans la partie 2 de ce post.