/// <summary> If the setting is found in the <paramref name="settingsStore"/>, retrieves the value of the setting, /// converts or deserializes it to the type of the wrapped property, and calls the property set method on /// the <paramref name="baseOptionModel"/>. No exceptions should be thrown from /// this method. No changes to the property will be made if the setting does not exist. </summary> /// <typeparam name="TOptMdl"> Type of the base option model. </typeparam> /// <param name="baseOptionModel"> The base option model which is used as the target object on which the property /// will be set. It also can be used for deserialization of stored data. </param> /// <param name="settingsStore"> The settings store to retrieve the setting value from. </param> /// <returns> True if the value exists in the <paramref name="settingsStore"/>, and the property was updated in /// <paramref name="baseOptionModel"/>, false if setting does not exist or any step of the process /// failed. </returns> public virtual bool Load <TOptMdl>(BaseOptionModel <TOptMdl> baseOptionModel, SettingsStore settingsStore) where TOptMdl : BaseOptionModel <TOptMdl>, new() { string collectionName = OverrideCollectionName ?? baseOptionModel.CollectionName; object?value = null; try { if (!settingsStore.PropertyExists(collectionName, PropertyName)) { return(false); } value = SettingStoreGetMethod(settingsStore, collectionName, PropertyName); value = ConvertStorageTypeToPropertyType(value, baseOptionModel); WrappedPropertySetMethod(baseOptionModel, value); return(true); } catch (Exception ex) { ex.Log("BaseOptionModel<{0}>.{1} CollectionName:{2} PropertyName:{3} dataType:{4} PropertyType:{5} Value:{6}", baseOptionModel.GetType().FullName, nameof(Load), collectionName, PropertyName, DataType, PropertyInfo.PropertyType, value ?? "[NULL]"); } return(false); }
/// <summary> The value of the wrapped property is retrieved by calling the property get method on <paramref name="baseOptionModel"/>. /// This value is converted or serialized to a native type supported by the <paramref name="settingsStore"/>, /// then persisted to the store, assuring the collection exists first. No exceptions should be thrown from /// this method. </summary> /// <typeparam name="TOptMdl"> Type of the base option model. </typeparam> /// <param name="baseOptionModel"> The base option model which is used as the target object from which the property /// value will be retrieved. It also can be used for serialization of stored data. </param> /// <param name="settingsStore"> The settings store to set the setting value in. </param> /// <returns> True if we were able to persist the value in the store. However, if the serialization results in a null value, /// it cannot be persisted in the settings store and false will be returned. False is also returned if any step /// of the process failed, and these are logged. </returns> public virtual bool Save <TOptMdl>(BaseOptionModel <TOptMdl> baseOptionModel, WritableSettingsStore settingsStore) where TOptMdl : BaseOptionModel <TOptMdl>, new() { string collectionName = OverrideCollectionName ?? baseOptionModel.CollectionName; object?value = null; try { value = WrappedPropertyGetMethod(baseOptionModel); value = ConvertPropertyTypeToStorageType(value, baseOptionModel); if (value == null) { Exception ex = new("Cannot store null in settings store."); ex.LogAsync("BaseOptionModel<{0}>.{1} CollectionName:{2} PropertyName:{3} dataType:{4} PropertyType:{5} Value:{6}", baseOptionModel.GetType().FullName, nameof(Load), collectionName, PropertyName, DataType, PropertyInfo.PropertyType, value ?? "[NULL]").Forget(); return(false); } // Rather than if ! CollectionExists then CreateCollection this is likely more efficient. settingsStore.CreateCollection(collectionName); SettingStoreSetMethod(settingsStore, collectionName, PropertyName, value); return(true); } catch (Exception ex) { ex.Log("BaseOptionModel<{0}>.{1} CollectionName:{2} PropertyName:{3} dataType:{4} PropertyType:{5} Value:{6}", baseOptionModel.GetType().FullName, nameof(Load), collectionName, PropertyName, DataType, PropertyInfo.PropertyType, value ?? "[NULL]"); } return(false); }
/// <summary> /// Creates a new instance of the options page. /// </summary> public BaseOptionPage() { _model = ThreadHelper.JoinableTaskFactory.Run(BaseOptionModel <T> .CreateAsync); }
/// <summary>Creates a new instance of the options page.</summary> public BaseOptionPage() { #pragma warning disable VSTHRD104 // Offer async methods _model = ThreadHelper.JoinableTaskFactory.Run(BaseOptionModel <T> .CreateAsync); #pragma warning restore VSTHRD104 // Offer async methods }
/// <summary> Convert the <paramref name="settingsStoreValue"/> retrieved from the settings store to the type of the /// property we are wrapping. See remarks at <see cref="ConvertPropertyTypeToStorageType{T}"/></summary> /// <typeparam name="TOptMdl"> Type of <see cref="BaseOptionModel{TOptMdl}"/>. </typeparam> /// <param name="settingsStoreValue"> The value retrieved from the settings store, as an object. This will not be null. </param> /// <param name="baseOptionModel"> Instance of <see cref="BaseOptionModel{TOptMdl}"/>. For types requiring deserialization, methods in this object are used. </param> /// <returns> <paramref name="settingsStoreValue"/>, converted to the property type. </returns> protected virtual object?ConvertStorageTypeToPropertyType <TOptMdl>(object settingsStoreValue, BaseOptionModel <TOptMdl> baseOptionModel) where TOptMdl : BaseOptionModel <TOptMdl>, new() { Type typeOfWrappedProperty = PropertyInfo.PropertyType; if (typeOfWrappedProperty.IsEnum) { typeOfWrappedProperty = typeOfWrappedProperty.GetEnumUnderlyingType(); } switch (DataType) { case SettingDataType.Serialized: if (NativeStorageType != NativeSettingsType.String) { throw new InvalidOperationException($"The SettingDataType of Serialized must be SettingsType.String. Was: {NativeStorageType}"); } return(baseOptionModel.DeserializeValue((string)settingsStoreValue, typeOfWrappedProperty, PropertyName)); case SettingDataType.Legacy: if (NativeStorageType != NativeSettingsType.String) { throw new InvalidOperationException($"The SettingDataType of Legacy must be SettingsType.String. Was: {NativeStorageType}"); } return(LegacyDeserializeValue((string)settingsStoreValue, typeOfWrappedProperty)); } if (TypeConverter != null) { Type valueType = settingsStoreValue.GetType(); if (NativeStorageType == NativeSettingsType.Binary) { // Type converter uses byte[] so extract byte array and set the conversion type. valueType = typeof(byte[]); settingsStoreValue = ((MemoryStream)settingsStoreValue).ToArray(); } if (!TypeConverter.CanConvertFrom(valueType)) { throw new InvalidOperationException($"TypeConverter {TypeConverter.GetType().FullName} can not convert from {valueType.Name} to {typeOfWrappedProperty.FullName}."); } object?returnObject = TypeConverter.ConvertFrom(null !, CultureInfo.InvariantCulture, settingsStoreValue); if (returnObject == null) { if (typeOfWrappedProperty.IsValueType) { throw new InvalidOperationException($"TypeConverter {TypeConverter.GetType().FullName} attempt to convert from {valueType.Name} to {typeOfWrappedProperty.FullName} returned null for a value type."); } return(returnObject); } if (!typeOfWrappedProperty.IsInstanceOfType(returnObject)) { throw new InvalidOperationException($"TypeConverter {TypeConverter.GetType().FullName} attempt to convert from {valueType.Name} to {typeOfWrappedProperty.FullName} returned incompatible type {returnObject.GetType().FullName}."); } return(returnObject); } switch (NativeStorageType) { case NativeSettingsType.Int32: if (typeOfWrappedProperty == typeof(Color)) { return(Color.FromArgb((int)settingsStoreValue)); } break; case NativeSettingsType.Int64: if (typeOfWrappedProperty == typeof(DateTime)) { return(DateTime.FromBinary((long)settingsStoreValue)); } break; case NativeSettingsType.String: if (typeOfWrappedProperty == typeof(Guid)) { return(Guid.Parse((string)settingsStoreValue)); } if (typeOfWrappedProperty == typeof(DateTimeOffset)) { return(DateTimeOffset.Parse((string)settingsStoreValue, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind)); } break; case NativeSettingsType.Binary: if (typeOfWrappedProperty == typeof(MemoryStream)) { return((MemoryStream)settingsStoreValue); } if (typeOfWrappedProperty == typeof(byte[])) { return(((MemoryStream)settingsStoreValue).ToArray()); } throw new InvalidCastException($"Can not convert SettingsType.Binary to {typeOfWrappedProperty.FullName} - property type must be byte[] or MemoryStream."); } if (typeOfWrappedProperty.IsInstanceOfType(settingsStoreValue)) { return(settingsStoreValue); } return(Convert.ChangeType(settingsStoreValue, typeOfWrappedProperty, CultureInfo.InvariantCulture)); }
/// <summary> Convert the <paramref name="propertyValue"/> retrieved from the property to the type it will be stored as in the /// <see cref="SettingsStore"/>. </summary> /// <typeparam name="TOptMdl"> Type of <see cref="BaseOptionModel{TOptMdl}"/>. </typeparam> /// <param name="propertyValue"> The value retrieved from the wrapped property, as an object. </param> /// <param name="baseOptionModel"> Instance of <see cref="BaseOptionModel{TOptMdl}"/>. For types requiring serialization, methods in this object are used. </param> /// <returns> <paramref name="propertyValue"/>, converted to one of the types supported by <see cref="SettingsStore"/>. </returns> /// <remarks> /// The methods <see cref="ConvertPropertyTypeToStorageType{T}" />, <see cref="ConvertStorageTypeToPropertyType{T}" />, and <see cref="InferDataType"/> are designed to /// work in tandem, and are therefore tightly coupled. The <see cref="SettingsStore"/> cannot store null values, therefore any property that is converted /// to a reference type cannot round-trip successfully if that conversion yields <see cref="string"/>, <see cref="MemoryStream"/>, and arrays of /// <see cref="byte"/> - in these cases the equivalent of <c>empty</c> is stored, therefore when loaded the result will not match. /// <para /> /// The method <see cref="InferDataType"/> returns an enumeration that identifies both the native storage type, and method of conversion, that /// will be used when storing the property value. These defaults can be overridden via the <see cref="OverrideDataTypeAttribute"/>. /// <para /> /// The method <see cref="ConvertPropertyTypeToStorageType{T}" /> is provided the current value of the property. It's job is to convert this value to /// the native storage type based on <see cref="DataType"/> which is set via <see cref="InferDataType"/>. /// <para /> /// The method <see cref="ConvertStorageTypeToPropertyType{T}" /> is the reverse of the above. Given an instance of the native storage type, /// it's job is to convert it to an instance the property type. /// <para /> /// The conversions between types in the default implementation follows this: /// <list type="bullet"> /// <item> <description>A property with a setting data type of <see cref="SettingDataType.Legacy"/> uses <see cref="BinaryFormatter"/> and stores it as a base64 encoded string. <see langword="null"/> values are stored as an empty string. </description></item> /// <item> <description>Array of <see cref="byte"/> is wrapped in a <see cref="MemoryStream"/>. <see langword="null"/> values are converted to an empty <see cref="MemoryStream"/>.</description></item> /// <item> <description><see cref="Color"/>, with setting data type <see cref="SettingDataType.Int32"/> uses To[From]Argb to store it as an Int32.</description></item> /// <item> <description><see cref="Guid"/>, with setting data type <see cref="SettingDataType.String"/> uses <see cref="Guid.ToString()"/> and <see cref="Guid.Parse"/> to convert to and from a string.</description></item> /// <item> <description><see cref="DateTime"/>, with setting data type <see cref="SettingDataType.Int64"/> uses To[From]Binary to store it as an Int64.</description></item> /// <item> <description><see cref="DateTimeOffset"/>, with setting data type <see cref="SettingDataType.String"/> uses the round-trip 'o' specifier to store as a string.</description></item> /// <item> <description><see cref="float"/> and <see cref="double"/>, with setting data type <see cref="SettingDataType.String"/> uses the round-trip 'G9' and 'G17' specifier to store as a string, and is parsed via the standard Convert method.</description></item> /// <item> <description><see cref="string"/>, if null, is stored as an empty string.</description></item> /// <item> <description>Enumerations are converted to/from their underlying type.</description></item> /// <item> <description><a href="https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/integral-numeric-types">Integral numeric types</a>, /// <see cref="float"/>, <see cref="double"/>, <see cref="decimal"/>, and <see cref="char"/> /// use <see cref="Convert.ChangeType(object, Type, IFormatProvider)" />, using <see cref="CultureInfo.InvariantCulture"/>. Enumerations are /// stored as their underlying integral numeric type.</description></item> /// <item> <description>Any type not described above, or a property with a setting data type of <see cref="SettingDataType.Serialized"/> /// uses <see cref="BaseOptionModel{T}.SerializeValue"/> and <see cref="BaseOptionModel{T}.DeserializeValue"/> and stores it as binary, /// refer to those overridable methods for details.</description></item> /// </list> /// </remarks> protected virtual object ConvertPropertyTypeToStorageType <TOptMdl>(object?propertyValue, BaseOptionModel <TOptMdl> baseOptionModel) where TOptMdl : BaseOptionModel <TOptMdl>, new() { switch (DataType) { case SettingDataType.Serialized: if (NativeStorageType != NativeSettingsType.String) { throw new InvalidOperationException($"The SettingDataType of Serialized is not capable of supporting native storage type {NativeStorageType}"); } string serializedString = baseOptionModel.SerializeValue(propertyValue, PropertyInfo.PropertyType, PropertyName); if (serializedString == null) { throw new InvalidOperationException($"The SerializeValue method of {baseOptionModel.GetType().FullName} returned " + " a null value. This method cannot return null."); } return(serializedString); case SettingDataType.Legacy: if (NativeStorageType != NativeSettingsType.String) { throw new InvalidOperationException($"The SettingDataType of Legacy is not capable of supporting native storage type {NativeStorageType}"); } return(LegacySerializeValue(propertyValue)); } Type conversionType = NativeStorageType.GetDotNetType(); if (TypeConverter != null) { bool returnMemoryStream = false; if (NativeStorageType == NativeSettingsType.Binary) { // For binary, the type conversion should be to byte[], then we return a memory stream. returnMemoryStream = true; conversionType = typeof(byte[]); } if (!TypeConverter.CanConvertTo(conversionType)) { throw new InvalidOperationException($"TypeConverter {TypeConverter.GetType().FullName} can not convert {PropertyInfo.PropertyType.FullName} to {NativeStorageType} ({conversionType.Name})"); } object?convertedObj = TypeConverter.ConvertTo(null, CultureInfo.InvariantCulture, propertyValue, conversionType); if (convertedObj == null) { throw new InvalidOperationException($"TypeConverter {TypeConverter.GetType().FullName} returned null converting from {PropertyInfo.PropertyType.FullName} to {NativeStorageType} ({conversionType.Name}), which is not supported."); } if (!conversionType.IsInstanceOfType(convertedObj)) { throw new InvalidOperationException($"TypeConverter {TypeConverter.GetType().FullName} returned type {convertedObj.GetType().FullName} when converting from {PropertyInfo.PropertyType.FullName} to {NativeStorageType} ({conversionType.Name})."); } if (returnMemoryStream) { return(new MemoryStream((byte[])convertedObj)); } return(convertedObj); } switch (NativeStorageType) { case NativeSettingsType.Int32: if (propertyValue is Color color) { return(color.ToArgb()); } break; case NativeSettingsType.Int64: if (propertyValue is DateTime dt) { return(dt.ToBinary()); } break; case NativeSettingsType.String: if (propertyValue is Guid guid) { return(guid.ToString()); } if (propertyValue is DateTimeOffset dtOffset) { return(dtOffset.ToString("o", CultureInfo.InvariantCulture)); } if (propertyValue is float floatVal) { return(floatVal.ToString("G9", CultureInfo.InvariantCulture)); } if (propertyValue is double doubleVal) { return(doubleVal.ToString("G17", CultureInfo.InvariantCulture)); } if (propertyValue == null) { return(string.Empty); } break; case NativeSettingsType.Binary: if (propertyValue is byte[] bytes) { return(new MemoryStream(bytes)); } if (propertyValue is MemoryStream memStream) { return(memStream); } if (propertyValue == null) { return(new MemoryStream()); } throw new InvalidOperationException($"Can not convert NativeStorageType of Binary to {propertyValue.GetType().FullName} - property type must be byte[] or MemoryStream."); } if (propertyValue == null) { throw new InvalidOperationException($"A null property value with SettingDataType of {DataType} is not supported."); } if (conversionType.IsInstanceOfType(propertyValue)) { return(propertyValue); } return(Convert.ChangeType(propertyValue, conversionType, CultureInfo.InvariantCulture)); }