Introduction

This post explains the preferences service available in IIS Manager. The Preference service allows developers to save and load preferences across instances of their modules.  The settings are stored in a file in the user’s windows profile so they can be accessed later even after inetmgr is closed and opened again. The type of data that can stored is limited to bools, ints and strings but you can easily serialize other data structures to a string which enables you store just about any type of data.

In this article we’ll walk through how the preferences service works and how to save some of your own preferences.

 

Getting started using the preferences service

Let’s create a page that lists a group of files to illustrate our point. We’ll also allow the user to group the files by whether they’re hidden or visible. Our list page looks like:

namespace PreferencesDemo {

    class PreferencesDemoPage : ModuleListPage {

 

        private ColumnHeader _nameColumn;

        private ColumnHeader _creationDateColumn;

        private ColumnHeader _modificationDateColumn;

        private ColumnHeader _readOnlyColumn;

        private ColumnHeader _hiddenColumn;

 

        private ModuleListPageGrouping _hiddenGrouping;

 

        private ListViewGroup _hiddenYes;

        private ListViewGroup _hiddenNo;

 

        public override ModuleListPageGrouping[] Groupings {

            get {

                if (_hiddenGrouping == null) {

                    _hiddenGrouping = new ModuleListPageGrouping("HiddenType", "Hidden");

 

                    _hiddenYes = new ListViewGroup("Hidden Files");

                    _hiddenNo = new ListViewGroup("Visible Files");

                }

 

                return new ModuleListPageGrouping[] {

                               _hiddenGrouping,

                           };

            }

        }

 

        protected override ListViewGroup[] GetGroups(ModuleListPageGrouping grouping) {

            if (grouping == _hiddenGrouping) {

                return new ListViewGroup[] {

                    _hiddenYes,

                    _hiddenNo,

                };

            }

 

            return null;

        }

 

        protected override void InitializeListPage() {

            _nameColumn = new ColumnHeader();

            _nameColumn.Text = "Name";

            _nameColumn.Width = 175;

 

            _creationDateColumn = new ColumnHeader();

            _creationDateColumn.Text = "Creation Date";

            _creationDateColumn.Width = 135;

 

            _modificationDateColumn = new ColumnHeader();

            _modificationDateColumn.Text = "Modification Date";

            _modificationDateColumn.Width = 135;

 

            _readOnlyColumn = new ColumnHeader();

            _readOnlyColumn.Text = "Read Only";

            _readOnlyColumn.Width = 65;

 

            _hiddenColumn = new ColumnHeader();

            _hiddenColumn.Text = "Hidden";

            _hiddenColumn.Width = 65;

 

            ListView.Columns.AddRange(new ColumnHeader[] {

                _nameColumn,

                _creationDateColumn,

                _modificationDateColumn,

                _readOnlyColumn,

                _hiddenColumn,

            });

 

            ListView.MultiSelect = false;

            ListView.LabelWrap = false;

        }

 

        protected override void OnGroup(ModuleListPageGrouping grouping) {

            foreach (ListViewItem item in ListView.Items) {

                if (grouping == _hiddenGrouping) {

                    // set item.Group to appropriate group

                }

            }

        }

    }

 

For simplicity we haven’t actually implemented any methods to populate the list view but that isn’t necessary for the purpose of illustrating the preference services of IIS Manager.

If you create a module out of this code and play around with it you might notice that our PreferencesDemo module doesn’t remember the user’s last “group by” setting unlike other list view pages.  This is because we haven’t given our module a ModulePageIdentifier attribute for the preferences service to use. Place the following code above the definition of the PreferencesDemoPage class and reinstall the new module.

[ModulePageIdentifier("03b8b54f-5682-4be1-83ec-ce1b0ef8ce58")]

You’ll notice the “group by” setting is now automatically remembered. This is done by the ModuleListPage base class. How does this work? The answer is the PreferencesStore which is described in the next section.

How the PreferencesStore works

Our “group by” setting was saved because once you define a  ModulePageIdentifier for your page the ModulePage base class will call SavePreferences from ModulePage::OnDeactivating and LoadPreferences from ModulePage::OnActivated (during the initial activation only). The function signatures are:

protected override void SavePreferences(PreferencesStore store)
protected virtual void LoadPreferences(PreferencesStore store)

ModuleListPage already overrides these methods and stores preferences like the last “group by” setting, column widths and previous filter queries on modules that use filter functionality like the Application Pools page.

These settings are stored on the local machine under the windows user’s “ApplicationData\Microsoft\WebManagement\<WebManagement version>\” folder in a file called InetMgr.preferences. The data is stored in a binary format so you won’t be able to edit the file directly. The data stored does not have any state associated with it so whatever preferences you store will be loaded by your module page regardless of the connection used by IIS Manager (e.g. settings saved while using IIS Manager as administrator on the local box will be loaded next time you use IIS Manager to connect to a remote server as a different user).

Saving preferences to the PreferencesStore

Now let’s say we want to allow the user to rearrange the columns on our list view page. The first step is to let the user reorder the columns. This is done by adding:

ListView.AllowColumnReorder = true;

In the InitializeListPage function.

Next we’ll define a helper class that will take care of getting and setting the column order:

namespace PreferencesDemo {

    class ColumnOrderHelper {

 

        [DllImport("user32.dll")]        

        static extern bool SendMessage(IntPtr hWnd, Int32 msg, Int32 wParam, int[] lParam);

 

        const int LVM_FIRST = 0x1000;

        const int LVM_SETCOLUMNORDERARRAY = LVM_FIRST + 58;

        const int LVM_GETCOLUMNORDERARRAY = LVM_FIRST + 59;

 

        //returns an array of column index

        public static int[] GetDisplayColumnOrders(ListView listView) {

 

            int numColumns = listView.Columns.Count;

            int[] returnValue = new int[numColumns];

            SendMessage(listView.Handle, LVM_GETCOLUMNORDERARRAY, numColumns, returnValue);      

            return returnValue;

        }

 

        public static bool SetDisplayColumnOrder(ListView listView, int[] columnOrder) {

 

            int numColumns = listView.Columns.Count;

 

            if (columnOrder.Length != numColumns) {

                return false;

            }

 

            return SendMessage(listView.Handle, LVM_SETCOLUMNORDERARRAY, numColumns, columnOrder);

        }

    }

}

 

Now we have to save the column order to the preferences store. The preferences store has three methods you can use to save preferences:

SetValue(string name, bool value, bool defaultValue)
SetValue(string name, int value, int defaultValue)
SetValue(string name, string value, string defaultValue)

Note that SetValue takes a defaultValue just like GetValue does. If you set a value to the defaultValue then the preferences store will delete that value’s entry since it is not needed.

 As mentioned earlier you can save bools, ints and strings to the preferences store. We’ll store our column order as a string of comma separated values:

protected override void SavePreferences(PreferencesStore store) {

            base.SavePreferences(store);

 

            int[] columnOrder = ColumnOrderHelper.GetDisplayColumnOrders(ListView);

 

            // translate to a string of csv

            string columnOrderString = String.Empty;

            for(int i = 0; i < columnOrder.Length; i++) {

                if (i > 0) {

                    columnOrderString += ",";

                }

 

                columnOrderString += columnOrder[i].ToString();

            }

 

            store.SetValue(“ColumnOrderKey”, columnOrderString, String.Empty);

        }

Notice we start our method with a call to base.SavePreferences so that our ModuleListPage settings will continue to be saved.

Next we’ll write our LoadPreferences method:

        protected override void LoadPreferences(PreferencesStore store) {

            base.LoadPreferences(store);

 

            string columnOrderCSV = store.GetValue(PreferencesDemoGlobals.ColumnOrderKey, String.Empty);

 

            // String.Empty is the default value meaning we don't have any real preferences saved

            if (!String.IsNullOrEmpty(columnOrderCSV)) {

                string[] columnOrderStringArray = columnOrderCSV.Split(',');

                _loadedColumnOrder = new int[columnOrderStringArray.Length];

 

                // can't actually SET the column order here yet because we haven't run

                // InitializePage() yet so the ListView doesn't have it's columns yet.

                for (int i = 0; i < columnOrderStringArray.Length; i++) {

                    _loadedColumnOrder[i] = Int32.Parse(columnOrderStringArray[i]);

                }

            }

        }

 

Note we don’t actually load the column order yet, we just save it to an int[] named _loadedColumnOrder. This is done because our ListView object doesn’t actually have any columns defined yet so we can’t change the column order. Our columns are added to the list view object in InitializeListPage which is called by ModuleListPage::OnActivated but LoadPreferences is called by ModulePage::OnActivated which runs first. Therefore the last step is to add code to load the list view column order after all of our preferences have been loaded and the page has been initialized:

        protected override void OnActivated(bool initialActivation) {

            base.OnActivated(initialActivation);

 

            if (initialActivation) {

                if (_loadedColumnOrder != null) {

                    ColumnOrderHelper.SetDisplayColumnOrder(ListView, _loadedColumnOrder);

                }

        }

Using the Preference service from the IServiceProvider

The preferences service can also be used from outside of a ModulePage.  If you have access to the IServiceProvider object you can get the IPreferencesService:

IPreferencesService preferencesService = (IPreferencesService)_serviceProvider.GetService(typeof(IPreferencesService);

Just like we had to define a Guid for our settings in the ModulePage, you’ll have to define a Guid and use that to access your preferences store:

Guid PreferenceStoreGuid = new Guid("9745f305-2b54-4fd0-86a8-de5527e9a7b3");
PreferencesStore store = preferencesService.GetPreferencesStore(PreferenceStoreGuid);

As a note, make sure to use the same Guid each time you access your preferences store otherwise you won’t be able to find your preferences later.

Now you can use the same PreferencesStore methods we were using before to save and load ints, strings and bools.  After setting values in the store you’ll have to call IPreferencesService.Save() to commit the changes.

In the sample code I’ve posted I’ve created a class that inherits from the IHomepageTaskListProvider interface and uses the preferences store just as I’ve described here. When the provider is instantiated it reads the value of the counter out of the preferences store, increments it by one and saves it again. It also provides a home page task to display the value of the counter. It doesn’t do anything useful but does demonstrate using the preferences service outside of a ModulePage as described above.

Conclusion

As you can see the preferences service is a convenient way of storing user specific settings that are persisted between inetmgr sessions.

Sample Code

Download the sample code here. If you open the solution and build it, it should build the two DLLs and register them in the GAC for you. Then open the %windir%\system32\inetsrv\config\administration.config file and put:

<add name="PreferencesDemo" type="PreferencesDemo.PreferencesDemoModuleProvider, PreferencesDemo, Version=0.0.0.0, Culture=neutral, PublicKeyToken=80e554de7f31e558" />

In the moduleProviders section with all the other module definitions. Then start inetmgr.exe and the Demo module should show up.