diff --git a/docs/website/binding_objc_libs.md b/docs/website/binding_objc_libs.md index 0a7c2dce1047..7c7a24778b21 100644 --- a/docs/website/binding_objc_libs.md +++ b/docs/website/binding_objc_libs.md @@ -779,6 +779,67 @@ interface LonelyClass { +## Binding Enums + +You can add `enum` directly in your binding files to makes it easier +to use them inside API definitions - without using a different source +file (that needs to be compiled in both the bindings and the final +project). + +Example: + +``` +[Native] // needed for enums defined as NSInteger in ObjC +enum MyEnum {} + +interface MyType { + [Export ("initWithEnum:")] + IntPtr Constructor (MyEnum value); +} +``` + +It is also possible to create your own enums to replace `NSString` +constants. In this case the generator will **automatically** create the +methods to convert enums values and NSString constants for you. + +Example: + +``` +enum NSRunLoopMode { + + [DefaultEnumValue] + [Field ("NSDefaultRunLoopMode")] + Default, + + [Field ("NSRunLoopCommonModes")] + Common, + + [Field (null)] + Other = 1000 +} + +interface MyType { + [Export ("performForMode:")] + void Perform (NSString mode); + + [Wrap ("Perform (mode.GetConstant ())")] + void Perform (NSRunLoopMode mode); +} +``` + +In the above example you could decide to decorate `void Perform (NSString mode);` +with an `[Internal]` attribute. This will **hide** the constant-based API +from your binding consumers. + +However this would limit subclassing the type as the nicer API alternative +uses a `[Wrap]` attribute. Those generated methods are not `virtual`, i.e. +you won't be able to override them - which may, or not, be a good choice. + +An alternative is to mark the original, `NSString`-based, definition as +`[Protected]`. This will allow subclassing to work, when required, and +the wrap'ed version will still work and call the overriden method. + + ## Binding Notifications Notifications are messages that are posted to the diff --git a/docs/website/binding_types_reference_guide.md b/docs/website/binding_types_reference_guide.md index 98fe03b4dfff..a19a987666f8 100644 --- a/docs/website/binding_types_reference_guide.md +++ b/docs/website/binding_types_reference_guide.md @@ -1952,6 +1952,105 @@ interface MyMutableString { } ``` +# Enum Attributes + +Mapping `NSString` constants to enum values is a easy way to create better +.NET API. It: + +* allows code completion to be more useful, by showing **only** the correct values for the API; +* adds type safety, you cannot use another `NSString` constant in a incorrect context; and +* allows to hide some constants, making code completion show shorter API list without losing functionality. + +Example: + +``` +enum NSRunLoopMode { + + [DefaultEnumValue] + [Field ("NSDefaultRunLoopMode")] + Default, + + [Field ("NSRunLoopCommonModes")] + Common, + + [Field (null)] + Other = 1000 +} +``` + +From the above binding definition the generator will create the `enum` itself and will +also create a `*Extensions` static type that includes two-ways conversion methods +between the enum values and the `NSString` constants. This means the constants remains +available to developers even if they are not part of the API. + +Examples: + +``` +// using the NSString constant in a different API / framework / 3rd party code +CallApiRequiringAnNSString (NSRunLoopMode.Default.GetConstant ()); +``` + +``` +// converting the constants from a different API / framework / 3rd party code +var constant = CallApiReturningAnNSString (); +// back into an enum value +CallApiWithEnum (NSRunLoopModeExtensions.GetValue (constant)); +``` + +## DefaultEnumValueAttribute + +You can decorate **one** enum value with this attribute. This will become the constant +being returned if the enum value is not known. + +From the example above: + +``` +var x = (NSRunLoopMode) 99; +Call (x.GetConstant ()); // NSDefaultRunLoopMode will be used +``` + +If no enum value is decorated then a `NotSupportedException` will be thrown. + +## ErrorDomainAttribute + +Error codes are bound as an enum values. There's generally an error domain for them +and it's not always easy to find which one applies (or if one even exists). + +You can use this attribute to associate the error domain with the enum itself. + +Example: + +``` + [Native] + [ErrorDomain ("AVKitErrorDomain")] + public enum AVKitError : nint { + None = 0, + Unknown = -1000, + PictureInPictureStartFailed = -1001 + } +``` + +You can then call the extension method `GetDomain` to get the domain constant of +any error. + +## FieldAttribute + +This is the same `[Field]` attribute used for constants inside type. It can also +be used inside enums to map a value with a specific constant. + +A `null` value can be used to specify which enum value should be returned if a +`null` `NSString` constant is specified. + +From the example above: + +``` +var constant = NSRunLoopMode.NewInWatchOS3; // will be null in watchOS 2.x +Call (NSRunLoopModeExtensions.GetValue (constant)); // will return 1000 +``` + +If no `null` value is present then an `ArgumentNullException` will be thrown. + + # Global Attributes Global attributes are either applied using the `[assembly:]` attribute modifier diff --git a/src/error.cs b/src/error.cs index c2810b037c30..23a517bcbcee 100644 --- a/src/error.cs +++ b/src/error.cs @@ -59,6 +59,7 @@ // BI1042 Missing '[Field (LibraryName=value)]' for {field_pi.Name} (e.g."__Internal") // BI1043 Repeated overload {mi.Name} and no [DelegateApiNameAttribute] provided to generate property name on host class. // BI1044 Repeated name '{apiName.Name}' provided in [DelegateApiNameAttribute]. +// BI1045 Only a single [DefaultEnumValue] attribute can be used inside enum {type.Name}. // BI11xx warnings // BI1101 Trying to use a string as a [Target] // BI1102 Using the deprecated EventArgs for a delegate signature in {0}.{1}, please use DelegateName instead diff --git a/src/generator-enums.cs b/src/generator-enums.cs index 39cee8f3dde0..19e726b4b92d 100644 --- a/src/generator-enums.cs +++ b/src/generator-enums.cs @@ -21,6 +21,14 @@ public ErrorDomainAttribute (string domain) public string ErrorDomain { get; set; } } +[AttributeUsage (AttributeTargets.Field)] +public class DefaultEnumValueAttribute : Attribute { + + public DefaultEnumValueAttribute () + { + } +} + public partial class Generator { static string GetCSharpTypeName (Type type) @@ -64,6 +72,8 @@ void GenerateEnum (Type type) } var fields = new Dictionary (); + Tuple null_field = null; + Tuple default_symbol = null; print ("public enum {0} : {1} {{", type.Name, GetCSharpTypeName (Enum.GetUnderlyingType (type))); indent++; foreach (var f in type.GetFields ()) { @@ -77,7 +87,15 @@ void GenerateEnum (Type type) continue; if (f.IsUnavailable ()) continue; - fields.Add (f, fa); + if (fa.SymbolName == null) + null_field = new Tuple (f, fa); + else + fields.Add (f, fa); + if (GetAttribute (f) != null) { + if (default_symbol != null) + throw new BindingException (1045, true, $"Only a single [DefaultEnumValue] attribute can be used inside enum {type.Name}."); + default_symbol = new Tuple (f, fa); + } } indent--; print ("}"); @@ -89,7 +107,7 @@ void GenerateEnum (Type type) // the *Extensions has the same version requirement as the enum itself PrintPlatformAttributes (type); print ("[CompilerGenerated]"); - print ("static public class {0}Extensions {{", type.Name); + print ("static public partial class {0}Extensions {{", type.Name); indent++; // note: not every binding namespace will start with ns.Prefix (e.g. MonoTouch.) @@ -146,13 +164,22 @@ void GenerateEnum (Type type) print ("public static NSString GetConstant (this {0} self)", type.Name); print ("{"); indent++; + print ("switch (self) {"); + var default_symbol_name = default_symbol?.Item2.SymbolName; foreach (var kvp in fields) { - print ("if (self == {0}.{1})", type.Name, kvp.Key.Name); + print ("case {0}.{1}:", type.Name, kvp.Key.Name); + var sn = kvp.Value.SymbolName; + if (sn == default_symbol_name) + print ("default:"); indent++; - print ("return {0};", kvp.Value.SymbolName); + print ("return {0};", sn); indent--; } - print ("return null;"); + print ("}"); + if (default_symbol_name == null) { + // note: a `[Field (null)]` does not need extra code + print ("return null;"); + } indent--; print ("}"); @@ -163,7 +190,11 @@ void GenerateEnum (Type type) indent++; print ("if (constant == null)"); indent++; - print ("throw new ArgumentNullException (nameof (constant));"); + // if we do not have a enum value that maps to a null field then we throw + if (null_field == null) + print ("throw new ArgumentNullException (nameof (constant));"); + else + print ("return {0}.{1};", type.Name, null_field.Item1.Name); indent--; foreach (var kvp in fields) { print ("else if (constant == {0})", kvp.Value.SymbolName); @@ -171,7 +202,11 @@ void GenerateEnum (Type type) print ("return {0}.{1};", type.Name, kvp.Key.Name); indent--; } - print ("throw new NotSupportedException (constant + \" has no associated enum value in \" + nameof ({0}) + \" on this platform.\");", type.Name); + // if there's no default then we throw on unknown constants + if (default_symbol == null) + print ("throw new NotSupportedException (constant + \" has no associated enum value in \" + nameof ({0}) + \" on this platform.\");", type.Name); + else + print ("return {0}.{1};", type.Name, default_symbol.Item1.Name); indent--; print ("}"); }