API Design and Windows Metadata Exporter
You can see the state of the code (warning: very rough) on bitbucket.
Caveats
What worked
How much has changed?
await and async keywords demands this, but does have a lot of benefits.For people who have been doing .NET development, they should be familiar with the concept of a class library - a reusable set of classes which can be referenced in other projects.
The Windows Runtime APIs behave slightly different (more on this later) and requires a different build process. Why do this? If you want your library to be consumed from C++ or a Javascript WinRT applications. Again, this is a topic far better covered by the product teams at BUILD, so go check them out.
When I change my project type to output a WinMD file, this changes the build process to include a tool called Windows Metadata Exporer (herein referred to as winmdexp).
You won't see this level of detail in your Output Window unless you switch the MSBuild output under Tools | Options | Projects and Solutions | Build and Run to Diagnostic or higher.
You will see the errors and warnings though:
Tips and Tricks
Sealed. Use It.
public class MyClass
{
public string MyProperty { get; set; }
}
you'll see this error: Type 'MyProject.MyClass' is unsealed but is not enabled for composition. Either seal 'MyProject.MyClass', or mark it with the System.Runtime.InteropServices.WindowsRuntime.EnableCompositionAttribute so that you can derive from it. To use the class from JavaScript, you must seal it.
At this point, you have two choices:
- If you don't care about supporting Javascript applications consuming your API - for example, you require consumers to subclass this type, add the
[EnableComposition]attribute to the class definition. - If you want to support all languages, add the sealed keyword to your class so that no subclassing is possible.
sealed keyword:
public sealed class MyClass
{
public string MyProperty { get; set; }
}
Notes: - As I worked my way through the codebase making the required changes, I was required to make conscious decisions around what I expose to consumers (the proxy and its APIs, the objects to return to the consumer), and what remains internal (helper functions, JSON parsing, etc) - the net result being a smaller footprint and more internal usage.
- The use of internal vs public for testability was a bit concerning - I hope the team have something in mind for doing an
[InternalsVisibleTo]-esque testing option - I need to try some edge-case scenarios around using the [EnableComposition] attribute - and how this impacts the consuming options
Program to an interface, not an implementation
public sealed class MyClass
{
public List<string> MyProperty { get; set; }
}
now we get multiple errors, such as: Method 'MyProject.MyClass.MyProperty.get()' has a parameter of type 'System.Collections.Generic.List<System.String>' in its signature. Although this type is not a valid Windows Runtime type, it implements interfaces which are valid Windows Runtime types. Consider changing the method signature to instead use one of the following types: 'System.Collections.Generic.IList<System.String>, System.Collections.Generic.IReadOnlyList<System.String>, System.Collections.Generic.IEnumerable<System.String>'.
Thankfully, the solution is right there in front of us.
Rather than rewriting the entire BCL, the wizards in the WinRT team have supported projecting the .NET interface to the Windows Runtime interface (more info here). So we can just use one of the suggested interfaces and the runtime takes care of the rest.
public sealed class MyClass
{
public IList<string> MyProperty { get; set; }
}
Notes: - Common sense - good to see this being enforced as part of the API
- There are a number of read-only collections being added to the Windows Runtime (and .NET Framework 4.5). Hooray!
Generic methods? Oh no you didn't!
public static class MyStaticClass
{
public static T Parse<T>(string text)
{
return default(T);
}
}
And we get scolded rather badly:'MyProject.MyStaticClass.Parse<T>(System.String)' is a generic method. Windows Runtime methods may not be generic.The Solution? Uh, don't do it.
Method 'MyProject.MyStaticClass.Parse<T>(System.String)' has a generic type parameter of type 'T' in its signature. Generic type parameters may not appear in method signatures of Windows Runtime methods.
Method 'MyProject.MyStaticClass.Parse<T>(System.String)' returns 'T', which is not a valid Windows Runtime type. Methods exposed to Windows Runtime must only return Windows Runtime types.
So, for all those MVVM frameworks who use helper methods like this:
protected void NotifyOfPropertyChange<TProperty>(Expression<Func<TProperty>> property)
{
NotifyOfPropertyChange(property.GetMemberInfo().Name);
}
need to fall back to the simple method of passing strings. I've railed about how this is a flawed concept before, so now its a priority (for me at least) to integrate AOP-esque workflows into this process - so that we can do away with the boilerplate code (and reflection-fu).
Or just mark the method as internal and don't expose it to the consumers - if the situation allows it.
Generic interfaces! Surely you can just...
public interface IPager<T> : IEnumerable<T>
{
int CurrentPage { get; }
int PageSize { get; }
int TotalItems { get; }
}
No concrete classes. Still using a generic interface though...Interface 'MyProject.IPager<T>' requires interface 'System.Collections.Generic.IEnumerable<T>' which is not a Windows Runtime interface. Exported interfaces may only require other Windows Runtime interfaces.
Type 'MyProject.IPager<T>' is generic. Windows Runtime types may not be generic.
Damn. Another brick wall.Notes:
- Why isn't this a supported scenario? Did I miss something?
- IIterable<T> is the complement of IEnumerable<T> - but it isn't part of the current build. Perhaps that is my issue...
- What generics are actually supported in WinRT? I haven't been able to find anything specific aside from a couple of slides in the afore-mentioned talks.
What about virtual methods?
[EnableComposition]
public class SomeClass
{
protected virtual void VirtualMethod(object param)
{
}
}
Aaaand... no. 'MyProject.SomeClass.VirtualMethod(System.Object)' is a virtual method. Managed types may not expose new virtual methods in Windows Runtime.Notes:
- Using
virtualis usually (IMO) the last-choice when doing OO design in .NET. Meh.
Fields! How do they work?
Empty Class?
public sealed class SomeClass
{
}
It will ask you what on earth you were thinking!
Unable to determine a default interface for runtime class 'MyProject.MyClass'. The class does not implement any interfaces, and no interface was generated for it because it does not contain any public methods, properties, or events.
Don't do async everywhere
public async Task<string> Get(string url)
{
var client = new HttpClient();
var response = await client.GetAsync(url);
return response.Content.ReadAsString();
}
But Task<T> is a BCL type, not a WinRT type:
Liquid error: undefined method `map' for #
public sealed class MyClass
{
public IAsyncOperation<string> Get(string url)
{
return AsyncInfoFactory.Create(() => GetInternal(url));
}
private static async Task<string> GetInternal(string url)
{
var client = new HttpClient();
var response = await client.GetAsync(url);
return response.Content.ReadAsString();
}
}
The public method exposes an IAsyncOperation return type, which is created by the factory class. This is compatible with the WinRT APIs, while retaining all the features of the TPL Task API. Notes:
- This smells like another candidate for some post-build weaving - keep the source code clean and rewrite after to satisfy the WinRT APIs.
- Can we rename AsyncInfoFactory to something more specific - IAsyncInfo is the base interface for everything this creates, but I kept forgetting the class name whenever I had to add it. AsyncFactory? AsyncOperationFactory?
What next?
- Watching this thread for an answer about my interface subclassing question.
- Investigating if the use of
sealedinterferes with testability - and if there's a good workaround. - Finish the damn library migration