none
Store array in options using DialogPage RRS feed

  • Question

  • Assume that I need to store any array in the extension just freshly created from the template.

    I just created new VSIX project, added VSPackage to it, then added option page grid (DialogPage). Then I followed instructions from answers to a similar question on StackOverflow: DialogPage - string array not persisted.

    And, for demonstration purposes, let's also add int[] array and plain int with custom type converter.

    // [standard attributes] [ProvideOptionPage(typeof(OptionPageGrid), "My Category", "My Grid Page", 0, 0, true)] public sealed class FooBarVSPackage : Package { // standard code } public class OptionPageGrid : DialogPage { // [typical attributes] [TypeConverter(typeof(StringArrayConverter))] public string[] Foos { get; set; } // [typical attributes] [TypeConverter(typeof(CustomIntConverter))] public int Bar { get; set; } // [typical attributes] [TypeConverter(typeof(IntArrayConverter))] public int[] Bazes { get; set; } } class StringArrayConverter : TypeConverter { // exact copy of code from similar question/answer mentioned above } public class IntArrayConverter : TypeConverter { private const string delimiter = "#@#"; // CanConvertFrom, ConvertTo, etc. overridden in similar fashion }

    public class CustomIntConverter : TypeConverter { // CanConvertFrom() overridden // CanConvertTo() overridden public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { var v = value as string; return int.Parse(v.TrimStart('*')); } public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) { var v = (int)value; return v.ToString().PadLeft(25, '*'); } }

    When I edit those options, I can see that the converter really works:

    Type Converter really works

    But after I reopen it, two of the values gone! Only plain int persisted:

    Array values are lost, but plain int persisted

    There is also one strange thing: how and when TypeConverter methods are called. CanConvertTo() is never called during the whole session. CanConvertFrom() and ConvertTo() are called often and more or less in expected fashion. And ConvertFrom() is called only when the string representation of the option is edited directly, i.e. it doesn't participate in loading/saving options at all!

    I'm not sure, but it feels a bit like int option is stored as int and turned from/into string only in options GUI, while array options just silently fail trying to do the same.

    P.S.: If you want to directly play with the example personally, here is a GitHub repo with the example project in question:

    FooBarVSIXProject.



    • Edited by MaximKamalov Saturday, September 26, 2015 12:26 PM put back lost “TypeConverter” and “}”, fixed typo in comment
    Friday, September 25, 2015 12:51 AM

Answers

  • After spending several hours trying to fix broken “easy to use” mechanism (either itself is broken or its documentation), I realized that instead of wasting time, I should have descend just one abstraction layer down and do exactly what I wanted DialogPage mechanism do automatically.

    One would expect that DialogPage should save/load the string representation (obtained through type converter) into/from (or something like that) when its SaveSettingsToStorage() and LoadSettingsFromStorage() are called. Since it refuses to do so and those methods are virtual, we can do exactly that ourselves:

        public class OptionPageGrid : DialogPage
        {
            const string collectionName = "FooBarVSIX";
        
            [Category("General")]
            [DisplayName("Foos")]
            [Description("Bla Foo Bla")]
            // note that TypeConverter attribute is removed,
            // because it's not relevant anymore
            public string[] Foos
            { get; set; }
    
            // Bar and Bazes properties missed out to make this example shorter
        
            public override void SaveSettingsToStorage()
            {
                base.SaveSettingsToStorage();
        
                var settingsManager = new ShellSettingsManager(ServiceProvider.GlobalProvider);
                var userSettingsStore = settingsManager.GetWritableSettingsStore(SettingsScope.UserSettings);
        
                if (!userSettingsStore.CollectionExists(collectionName))
                    userSettingsStore.CreateCollection(collectionName);
        
                var converter = new StringArrayConverter();
                userSettingsStore.SetString(
                    collectionName,
                    nameof(Foos),
                    converter.ConvertTo(this.Foos, typeof(string)) as string);
                // save Bazes in similar way
            }
        
            public override void LoadSettingsFromStorage()
            {
                base.LoadSettingsFromStorage();
        
                var settingsManager = new ShellSettingsManager(ServiceProvider.GlobalProvider);
                var userSettingsStore = settingsManager.GetWritableSettingsStore(SettingsScope.UserSettings);
        
                if (!userSettingsStore.PropertyExists(collectionName, nameof(Foos)))
                    return;
        
                var converter = new StringArrayConverter();
                this.Foos = converter.ConvertFrom(
                    userSettingsStore.GetString(collectionName, nameof(Foos))) as string[];
                // load Bazes in similar way
            }
        }
    Now, of course, if you do it this way, you don't have to write and use TypeConverter, actually. You can just embed serialization logic write into those methods, or anywhere.

    Also, you can serialize your data just right into binary format and use SetMemoryStream() to save it.
    • Marked as answer by MaximKamalov Thursday, October 29, 2015 6:22 PM
    Thursday, October 29, 2015 6:21 PM

All replies

  • Hi Maxim Kamalov,

    I found a mistake in your code. Change your code as follow. The destinationType must be string, not string[].

    public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
    {
        return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
    }
    Best Regards,
    Li Wang

    Friday, September 25, 2015 8:20 AM
    Moderator
  • I just fixed it and tried again, nothing changed. Probably, CanConvertTo() is really never called. Thank anyway.
    • Edited by MaximKamalov Saturday, September 26, 2015 12:24 PM rephrase a bit for better grammatical correctness
    Saturday, September 26, 2015 12:06 PM
  • After spending several hours trying to fix broken “easy to use” mechanism (either itself is broken or its documentation), I realized that instead of wasting time, I should have descend just one abstraction layer down and do exactly what I wanted DialogPage mechanism do automatically.

    One would expect that DialogPage should save/load the string representation (obtained through type converter) into/from (or something like that) when its SaveSettingsToStorage() and LoadSettingsFromStorage() are called. Since it refuses to do so and those methods are virtual, we can do exactly that ourselves:

        public class OptionPageGrid : DialogPage
        {
            const string collectionName = "FooBarVSIX";
        
            [Category("General")]
            [DisplayName("Foos")]
            [Description("Bla Foo Bla")]
            // note that TypeConverter attribute is removed,
            // because it's not relevant anymore
            public string[] Foos
            { get; set; }
    
            // Bar and Bazes properties missed out to make this example shorter
        
            public override void SaveSettingsToStorage()
            {
                base.SaveSettingsToStorage();
        
                var settingsManager = new ShellSettingsManager(ServiceProvider.GlobalProvider);
                var userSettingsStore = settingsManager.GetWritableSettingsStore(SettingsScope.UserSettings);
        
                if (!userSettingsStore.CollectionExists(collectionName))
                    userSettingsStore.CreateCollection(collectionName);
        
                var converter = new StringArrayConverter();
                userSettingsStore.SetString(
                    collectionName,
                    nameof(Foos),
                    converter.ConvertTo(this.Foos, typeof(string)) as string);
                // save Bazes in similar way
            }
        
            public override void LoadSettingsFromStorage()
            {
                base.LoadSettingsFromStorage();
        
                var settingsManager = new ShellSettingsManager(ServiceProvider.GlobalProvider);
                var userSettingsStore = settingsManager.GetWritableSettingsStore(SettingsScope.UserSettings);
        
                if (!userSettingsStore.PropertyExists(collectionName, nameof(Foos)))
                    return;
        
                var converter = new StringArrayConverter();
                this.Foos = converter.ConvertFrom(
                    userSettingsStore.GetString(collectionName, nameof(Foos))) as string[];
                // load Bazes in similar way
            }
        }
    Now, of course, if you do it this way, you don't have to write and use TypeConverter, actually. You can just embed serialization logic write into those methods, or anywhere.

    Also, you can serialize your data just right into binary format and use SetMemoryStream() to save it.
    • Marked as answer by MaximKamalov Thursday, October 29, 2015 6:22 PM
    Thursday, October 29, 2015 6:21 PM