Win10 apps in .NET - porting from phone8.1 to phone8.1 + Win10 using Shared Projects
This is Part 7 of my "VB Win10 Apps" series. (Everything applies to C# as well, of course).
- Part 1: getting started
- Part 2: issues with common libraries - JSON.Net, SignalR, SharpDX, SQLite, LiveSDK
- Part 3: design-time data in XAML
- Part 4: references
- Part 5: projects, targets, libraries
- Part 6: porting from 8.1 universal to Win10
- > Part 7: porting from Phone8.1 to Phone8.1+Win10 using Shared Projects
- Part 8: getting started with win2d accelerated graphics
- Part ?: ... (please let me know what you'd like me to write about next!)
Today I worked on porting from a Phone8.1 app, to also include Win10 "UWP". I did this because I figure a lot of my customers won't be able or willing to upgrade their Phone8.1 phones to Win10.Mobile any time soon so I had to keep that old target. So my app will now run on all these devices:
- Phone8.1, Win10.Desktop (for desktops and laptops), Win10.Mobile (for phones and tablets), and also Xbox and Hololens too I suppose.
There were two real highlights today that left me happy: (1) Debug PerfTips helped diagnose a perf issue and so cut startup time down from seven seconds to under a second. (2) Release build, which uses .NET Native under the hood, cut startup down still further to 110ms, basically imperceptible.
- Watch live coding screencast: porting from Phone8.1 to phone8.1+Win10 using Shared Projects [65mins]
- "Before" source code: https://github.com/ljw1004/blog/tree/master/PortToWin10/MyPicturesBreakout_Original_Phone81
- "After" source code: https://github.com/ljw1004/blog/tree/master/PortToWin10/MyPicturesBreakout_Final_Phone81andUWP
I know that 65 minutes is a long video to watch over my shoulder as I code and explain what I'm doing. I've extracted out the key points below, and noted where they occur in the video. If you'd like me to write a future blog-post that explores any of them in more detail, please let me know.
Project-structure: strategy for sharing code between Phone8.1 and UWP
- I have to write two apps, one for Phone8.1 and one for UWP.
- They will both be submitted to the store, with the same Application ID, as described in this link "What is a Universal App".
- Even though they're two separate apps, the store will offer the right one for download to each client device (so a Win10.Mobile phone will get the UWP version, while a Phone8.1 phone will get the Phone8.1 version).
- And because they share the same ApplicationID, any in-app purchases made in one will count as purchased in the other, and high-scores will roam from one to the other.
- What project-structure strategy should I use?
- Portable Class Libraries (PCLs) , under Add > New Project > VB > Windows > Windows8 > Windows > Class Library (Portable) . PCLs are about sharing a common binary project referenced by two apps. They are cleaner but involve more work.
- Shared Projects, under Add > New Project > VB > Shared Project. Shared Projects are about sharing source code between two apps. They are more quick-and-dirty. They are new to VB in VS2015.
- There are several steps involved to set up a Shared Project solution:
- [1:30] Add the Shared Project. Then, from each "head" project, References > Add Reference > Shared Projects.
- A good project naming scheme is "App1.Phone81, App1.UWP, App1.Shared". I advise not to use "App1.Windows" since that gives compilation errors...
- You can shift+drag files from one project to the shared project. You can also cut them out of one project, and paste them into the Shared Project.
- [5:20] Change the Root Namespace to be the same in each head project, and also in the Shared Project. Use just "App1" or whatever is your common prefix.
- [6:30] On each head, Project > Properties > Compiler > Advanced Compiler Options, and make sure it has a distinguishing #define.
- [7:15] Also change the Root Namespace in the appxmanifest
- [18:50] Shared Projects don't have their own References. So you have to add all the references you need (including NuGet references) to all head projects. [19:50] For Json.NET for now, use the latest stable 6.0.8 version. [20:25] For SharpDX for now, use the prerelease 3.0 version.
- I also change the folder names on disk for my projects: App1.Phone81, App1.UWP, App1.Shared. If necessary I edit the .sln file in notepad to correct its paths to the .vbproj files.
- Become familiar with the "Project Context Switcher", in the top-left of the code-editor window and the XAML designer window. This lets you see how shared code/xaml will look as part of either head project.
Manually merge app.xaml and app.xaml.vb
- RequestedTheme in App.xaml.
- [8:30] RequestTheme is typically absent from Phone8.1 projects, but set in UWP projects. You have to decide what you want. The behavior is: (Phone8.1 and Win10.Mobile) if this attribute is absent then the app uses either Light or Dark theme depending on the user's system-wide preference; (Win10.Desktop) if this attribute is absent then the app uses Light theme; (all) if this attribute is present then it overrides the user's system-wide preference.
- A completely minimal App.xaml.vb for a UWP project.
- [11:00] Removing AppInsights and System.Numerics.Vectors. These come by default when you do File>New>UWP, but it's easier to merge with Phone8.1 App.xaml.vb file if we remove them from our UWP project. We can always add them back in later when we're ready.
- [12:20] At this point the video shows a completely minimal App.xaml.vb for a UWP project.
Manually merge Assets folder, and Package.appxmanifest
- [27:00] Remember to copy the "capabilities" (e.g. InternetClient, PicturesLibrary) from your existing Phone8.1 project into the new UWP project.
- [27:25] In VS2015 RC, there's only a text-based appxmanifest editor for UWP apps. This will be upgraded to a proper visual editor in VS2015 RTM.
- [27:45] In UWP apps, some capabilities are listed under <Capability> and some under <uap:Capability> . No rhyme nor reason to it.
- The appxmanifest files are so different between Phone8.1 and UWP that it's not possible to share them. Each project needs its own appxmanifest.
- [31:20] As far as I can tell, for now, it doesn't make sense to merge standard Assets - Logo, SmallLogo, WideLogo, StoreLogo, SplashScreen. I kept them separate between Phone8.1 and UWP.
- Transparent, so as to use the user's theme color
- [49:20] You will probably want to change <uap:VisualElements BackgroundColor="transparent"> to be transparent. When it's transparent, then your asset files for logo &c. will be drawn on top of the user's preferred theme color, and so any transparencies in your PNG files will show the theme color. This is what Phone users expect of your app. It will also look better on Win10.Desktop.
- [51:30] There's a bug in Win10.Desktop where BackgroundColor="transparent" isn't respected for splash-screens. I don't know if it will be fixed.
- There's a bug in current versions of Win10.Mobile where the StatusBar background and foreground are out of sync, and you typically get white text (from the system-wide theme Dark theme preference) on a white background (from the app-requested Light theme preference). This will be fixed soon.
Writing an adaptive UI for a simple single-screen game
- Using the XAML designer effectively
- [16:45] In the XAML designer, use the project-context dropdown, and also the UWP-device-dropdown, to see how your app will look in Phone8.1, UWP-Phone, UWP-Tablet, UWP-Desktop.
- Wrestling with <Page Background> differences between Phone8.1 and UWP
- [17:30] The <Page Background="..."> attribute controls the background of the entire page, including the StatusBar area on Phone8.1 and Win10.Mobile. If you merely set the background of the root element inside your page (as is done by UWP templates), then it won't affect the StatusBar. It's up to you whether you want the StatusBar to have standard color that comes from the user's Light|Dark theme preference, or your own color. Note that the XAML designer incorrectly shows UWP apps as transparent even if they set <Page Background="..."> .
- [52:30] For some reason, when I set the background color of my root element, then the Phone8.1 version of my app didn't render correctly. I don't know why.
- Windowed mode is an easy way to see how your app will behave at all different sizes
- [33:25] I spent a while playing resizing my app in windowed mode on the desktop, to see how it looks at different screen sizes.
- [39:40] To set minimum size of your app while windowed, ApplicationView.GetForCurrentView().SetPreferredMinSize. This was handy because I didn't have to worry about my app having to adapt to even smaller sizes. You'll need to protect this call with an #ifdef if you're in shared code that's shared with Phone8.1, since this API doesn't exist on Phone8.1.
- Debugging a XAML bug I made...
- [34:00] I decided to add a nice background image, in case the window is wide and the (portrait) playing area doesn't fill it effectively.
- [36:45] This part of the video shows how I debugged a XAML problem, where my background image wasn't showing. I stuck in a dummy <Rectangle Width="300" Height="500" Fill="Yellow"/> and that was enough to show me what I'd done wrong.
- Adapting pointer input between touch and mouse modes
- [48:10] My phone app only ever had to deal with touch input, and it had a special adjustment to take into account the fatness of the finger touching it. Now that the app will be used by mouse users as well, I make this adjustment conditional upon e.Pointer.PointerDeviceType = PointerDeviceType.Touch.
- Give up on SupportedRotations
- [49:45] I used to set SupportedRotations=Portrait, to prevent my app from rotating. On Win10.Desktop, locked rotation is only respected if (1) the keyboard is detached, and (2) either the app is in FullScreen mode or the system is in Tablet mode. So you basically have to give up on locked rotations. You have to deal with your app window being resized to many different sizes, and make sure it looks good enough in all of them.
- #If: for compile-time choices about code that goes into one project but not another
- [21:00] In Shared Projects, intellisense and tooltips show "warning icons" for APIs that are only available in UWP, or more generally are only available in one head project.
- [39:40] In Shared Projects, you have to use #If to protect code blocks that use UWP APIs which are absent on one or other of the head projects.
- If you don't use #If, then the project will simply fail to compile.
- If: for run-time choices about code in a single binary that should run on one platform but not another.
- [21:30] For UWP, if you want to use any APIs that are only available on Win10.Mobile, or ones that are only available on Win10.Desktop, do References > Add Reference > Windows Universal > Extensions, and check "Mobile Extension SDK" or "Desktop Extension SDK" or both.
- These so-called "Platform Extension SDKs" do not force your app to run on those platforms. They merely allow you to adaptively light up and use the APIs specific to those platforms.
- [28:35] If you use an API from Mobile but try to run your app on Desktop, then it will throw a TypeLoadException exception at runtime. To prevent this, you must guard it with an "If" statement.
- [29:05] The easiest way to add the correct "If ApiInformation.IsTypePresent(...)" statement is with the PlatformSpecific.Analyzer NuGet package that I wrote.
- If you don't use If, then the project will compile okay, but will throw an exception at runtime.
- The method "ApiInformation.IsTypePresent" is itself absent from Phone8.1. So if you use it in code shared between Phone8.1 and UWP, you have to guard this If check within another #If check! I know it's confusing. Please watch the video.
- When to use #If vs If
- Use "#If" in a Shared Project that's shared between Phone8.1 and UWP, to select (at compile-time) between code that should be used when compiling for one or the other.
- Use "If" anywhere within a UWP-only project, to select (at run-time) between code that should run on one UWP device but not another.
- To help you get "#If" right, VS2015 provides warning icons in intellisense and tooltips, and gives live errors in the error-list when you get it wrong.
- To help you get "If" right, there's nothing in the box, so you have to use PlatformSpecific.Analyzer from NuGet. This gives live squiggles and warnings in the error-list when you get it wrong.
Principles of backwards compatibility of Windows 10
- Which binaries are backwards-compatible?
- Any Phone8.1 app and any Phone8.1 DLL will work on a Win10.Mobile device.
- Any Win8.1 app and any Win8.1 DLL will work on a Win10.Desktop device.
- Any Phone8.1+Win8.1 universal PCL will work on all Win10 devices.
- What source code is backwards-compatible?
- [20:45] The StatusBar type was present on Phone8.1 but seemed absent from UWP. However, we know the backwards-compatibility promise, that the type must be allowed on Win10.Mobile devices, so the way to access this API is by adding a reference to the Windows Mobile Platform Extension SDK.
- Back-compat bugs?
- [24:00] It looks like CurrentApp.LicenseInformation used to work fine on Win8.1 and Phone8.1 even if your app didn't have a store associating. It seems to be throwing an exception now in Win10. This must therefore be a back-compat bug in Win10. I expect it will be fixed.
Deployment and store submission
- What happened to AnyCPU?
- For libraries, you should continue to use AnyCPU.
- [22:15] For apps themselves, AnyCPU is no longer supported. If you try then it gives an error "The project needs to be deployed before it can be started. Verify the project is selected to be deployed in the Solution Configuration Manager".
- You should generally use x86 when deploying on LocalMachine and Emulator, and ARM when deploying to phone devices.
- When it comes time to submit to the store, the store-submission dialog will automatically package up all three versions x86|x64|ARM in the store submission. When an end-user downloads your app from the store, they'll automatically get the right one.
- I know that the lack of AnyCPU is a pain! Sorry!
- Deployment errors
- [51:00] Sometimes you get deployment errors such as DEP0001.
- A common fix is just to try deploying over again. Most of the time the errors go away.
- There seem to be more problems when you deploy to SD card on your phone. Switch the phone to use internal storage for new apps.
Improving app startup performance, through debugging and .NET Native
- I colored this title Red because it was so exciting!
- Debugging tools
- [41:05] The traditional tool for perf-debugging is Debug > Start Diagnostic Tools.
- [42:30] New in VS2015 is another tool: Perf Tips. Whenever you F10 over a statement, it shows you how many milliseconds that statement took to execute.
- Slow exception
- [24:00] Due to a bug in Windows, CurrentApp.LicenseInformation was throwing an exception. So I wrapped it up in a Try/Catch block.
- [44:30] PerfTips showed me that this exception was costing 2.8 seconds just to throw and catch. So I rewrote the code to avoid the throw.
- This is really weird. It's true that exceptions are costly, but they should never be this costly. I conjecture that Windows was just timing out internally on something or other.
- .NET Native startup time improvements
- [46:00] When you switch to Release build, it uses .NET Native. This is a highly aggressive whole-program optimizing compiler. It bypasses the CLR, and yields much faster startup times. Hopefully lower battery life too.
- My app startup time was reduced to about 100ms.
- .NET Native gives considerable boost to JSON.Net. Normally it takes about 200ms initialization time, the first time you invoke JsonConvert.DeserializeObject in your app. But with .NET Native that is slashed down to about 5ms. You read that right! It means that JSON.Net is now acceptable to call during app startup.
- Everyone should test their app in Release build. I try it about once every hour during development, to make sure I'm not doing something weird.
- Data-loss scenario with multiple devices
- [54:10] ApplicationData.RoamingsSettings is designed to let settings "roam" between any of a user's devices that have your app installed. This scenario will become more common with Win10. However, the roaming settings were only designed with simple settings in mind, e.g. "on/off switches", where it doesn't matter if the setting from one device overwrites that on another.
- I wanted to roam user's in-game performance between devices, to sum up the total length of time played in the game over all devices. This could suffer from data-loss if the game is newly installed on one device, which sends out the roaming setting "I have only been played for 15 seconds", and then overwrites the setting "I have been played for 36 hours" on another device.
- [55:40] My solution was that each device will roam its own contribution to the total amount of time played. That way there will be no data loss.
- Unique per-device key
- [56:15] There's no way to get a unique per-device identifier. (This would probably constitute privacy infringement). However, you can make a "per-app-install" unique identifier.
- Roaming between devices
- [57:55] If roaming-settings get roamed while your app is currently running, it receives a DataChanged event.
- [1:02:55] Unfortunately there's no way to test this yet at time of writing, because roaming only happens if you have submitted your app to the store.