In an earlier post we covered the four project templates that are included with the Icenium Extension for Visual Studio, and also linked to a set of alternative templates that are identical to the "official" ones that ship with the extension, minus the sample app boilerplate. Today, though, I want to cover what your options when you'd like to create your own custom project templates for building hybrid mobile apps with Icenium in Visual Studio.
Why would this be useful? Well - perhaps you use Angular.js or Backbone.js in your hybrid mobile projects, and you'd really love to have a starting point that includes the assets you need for you project, and perhaps even some basic configuration, app boilerplate and directory structure in place for you. Creating your own custom hybrid mobile template isn't hard - it just involves a little bit of effort. So where do we start?
It's a good idea to start with one of the existing project templates - since a lot of what they contain will be important for you to have. For the sake of this walkthrough, we're going to start with the built-in "Kendo UI Mobile" project template that ships with the Icenium Extension for Visual Studio. For my custom project template, I plan to use Kendo UI Mobile, along with a couple of other libraries I want to have on hand: postal.js and machina.js.
A Visual Studio Extension (a .vsix
file) is effectively a zip archive. In fact, you can view the contents of an extension if you rename the file extension to ".zip" instead of ".vsix".
In the screenshot sequence below, I renamed the extension to ".zip", extracted it, and then browsed the extracted files, to the "Project Templates" directory (where the four out-of-the-box templates can be found).
At this point, you need to extract the desired zip file into a working directory somewhere else on your file system. For example, I've extracted the Telerik.Mobile.Cordova.KendoUI.zip
file to a different directory and can see the following contents:
Now that I have this project template extracted, I can begin to modify the contents as needed.
The .vstemplate file contains information about the project structure and resources, which Visual Studio will depend on to properly create a new project based on this template. We're going to make the following changes to this file:
Name
element's value to "Kendo UI Mobile (w/postal and machina)".Description
element.TemplateID
element's value to "KendoUI.postal_machina".Folder
element from the original vstemplate file that created the "data" folder and added "weather.json" to it.Folder
element containing the assets for the images directory, remove the location, search and weather images.Folder
element containing the assets for the scripts directory, remove the location, login and weather files.Folder
element for the "scripts" directory, add items for machina.js and postal.js, as well as underscore.js (underscore is a dependency of both machina and postal).When the above changes have been applied, the .vstemplate file will look like this:
<?xml version="1.0" encoding="utf-8"?>
<VSTemplate Version="3.0.0" Type="Project" xmlns="http://schemas.microsoft.com/developer/vstemplate/2005">
<TemplateData>
<Name>Kendo UI Mobile (w/postal and machina)</Name>
<Description>Project template for creating hybrid mobile applications using Kendo UI Mobile, as well as machina.js and postal.js.</Description>
<Icon>Icon.ico</Icon>
<TemplateID>KendoUI.postal_machina</TemplateID>
<ProjectType>Icenium</ProjectType>
<SortOrder>20</SortOrder>
<NumberOfParentCategoriesToRollUp>1</NumberOfParentCategoriesToRollUp>
<CreateNewFolder>true</CreateNewFolder>
<DefaultName>MobileProject</DefaultName>
<ProvideDefaultName>true</ProvideDefaultName>
</TemplateData>
<TemplateContent>
<CustomParameters>
<CustomParameter Name="$isrunnable$" Value="true" />
</CustomParameters>
<Project File="mobile.iceproj" ReplaceParameters="true">
<Folder Name="App_Resources" TargetFolderName="App_Resources">
<Folder Name="iOS" TargetFolderName="iOS">
<ProjectItem ReplaceParameters="false" TargetFileName="icon.png">icon.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="icon@2x.png">icon@2x.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="icon-72.png">icon-72.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="icon-72@2x.png">icon-72@2x.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="Default.png">Default.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="Default@2x.png">Default@2x.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="Default-568h@2x.png">Default-568h@2x.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="Default-Portrait.png">Default-Portrait.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="Default-Landscape.png">Default-Landscape.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="Default-Portrait@2x.png">Default-Portrait@2x.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="Default-Landscape@2x.png">Default-Landscape@2x.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="Icon-Small.png">icon-small.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="Icon-Small@2x.png">icon-small@2x.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="Icon-Small-50.png">Icon-Small-50.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="Icon-Small-50@2x.png">Icon-Small-50@2x.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="icon-40.png">icon-40.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="icon-40@2x.png">icon-40@2x.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="icon-60.png">icon-60.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="icon-60@2x.png">icon-60@2x.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="icon-76.png">icon-76.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="icon-76@2x.png">icon-76@2x.png</ProjectItem>
</Folder>
<Folder Name="Android" TargetFolderName="Android">
<Folder Name="drawable-hdpi" TargetFolderName="drawable-hdpi">
<ProjectItem ReplaceParameters="false" TargetFileName="icon.png">icon.png</ProjectItem>
</Folder>
<Folder Name="drawable-mdpi" TargetFolderName="drawable-mdpi">
<ProjectItem ReplaceParameters="false" TargetFileName="icon.png">icon.png</ProjectItem>
</Folder>
<Folder Name="drawable-ldpi" TargetFolderName="drawable-ldpi">
<ProjectItem ReplaceParameters="false" TargetFileName="icon.png">icon.png</ProjectItem>
</Folder>
<Folder Name="drawable-nodpi" TargetFolderName="drawable-nodpi">
<ProjectItem ReplaceParameters="false" TargetFileName="splashscreen.9.png">splashscreen.9.png</ProjectItem>
</Folder>
</Folder>
</Folder>
<Folder Name="kendo" TargetFolderName="kendo">
<Folder Name="js" TargetFolderName="js">
<ProjectItem ReplaceParameters="false" TargetFileName="jquery.min.js">jquery.min.js</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="kendo.mobile.min.js">kendo.mobile.min.js</ProjectItem>
</Folder>
<Folder Name="styles" TargetFolderName="styles">
<Folder Name="images" TargetFolderName="images">
<ProjectItem ReplaceParameters="false" TargetFileName="back.png">back.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="KendoUI.ttf">KendoUI.ttf</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="KendoUI.woff">KendoUI.woff</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="wp8_icons.png">wp8_icons.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="wp8_inverseicons.png">wp8_inverseicons.png</ProjectItem>
</Folder>
<ProjectItem ReplaceParameters="false" TargetFileName="kendo.mobile.all.min.css">kendo.mobile.all.min.css</ProjectItem>
</Folder>
</Folder>
<Folder Name="styles" TargetFolderName="styles">
<ProjectItem ReplaceParameters="false" TargetFileName="main.css">main.css</ProjectItem>
<Folder Name="images" TargetFolderName="images">
<ProjectItem ReplaceParameters="false" TargetFileName="icenium.png">icenium.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="icenium2x.png">icenium2x.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="logo.png">logo.png</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="logo2x.png">logo2x.png</ProjectItem>
</Folder>
</Folder>
<Folder Name="scripts" TargetFolderName="scripts">
<ProjectItem ReplaceParameters="false" TargetFileName="app.js">app.js</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="underscore.min.js">underscore.min.js</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="machina.min.js">machina.min.js</ProjectItem>
<ProjectItem ReplaceParameters="false" TargetFileName="postal.min.js">postal.min.js</ProjectItem>
</Folder>
<ProjectItem ReplaceParameters="false" OpenInEditor="true">index.html</ProjectItem>
<ProjectItem ReplaceParameters="false" OpenInEditor="false">cordova.android.js</ProjectItem>
<ProjectItem ReplaceParameters="false" OpenInEditor="false">cordova.ios.js</ProjectItem>
</Project>
</TemplateContent>
</VSTemplate>
The mobile.iceproj
file is used to determine what assets are sent to Icenium's cloud build services (Content and Resource items are packaged and sent to the cloud, and project properties are pesisted here as well). It's important that we update this file to reflect the contents of our template, as we did with the .vstemplate file. We'll make the following changes:
When we're done with the above steps, our mobile.iceproj
file will look like this:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>$guid1$</ProjectGuid>
<ProjectTypeGuids>{070BCB52-5A75-4F8C-A973-144AF0EAFCC9}</ProjectTypeGuids>
<AppIdentifier>com.telerik.$safeappidentifier$</AppIdentifier>
<ProjectName>$projectname$</ProjectName>
<IsRunnable>$isrunnable$</IsRunnable>
<FrameworkVersion>3.0.0</FrameworkVersion>
<AndroidPermissions>
android.permission.CAMERA;android.permission.VIBRATE;android.permission.ACCESS_COARSE_LOCATION;android.permission.ACCESS_FINE_LOCATION;android.permission.ACCESS_LOCATION_EXTRA_COMMANDS;android.permission.READ_PHONE_STATE;android.permission.INTERNET;android.permission.RECEIVE_SMS;android.permission.RECORD_AUDIO;android.permission.MODIFY_AUDIO_SETTINGS;android.permission.READ_CONTACTS;android.permission.WRITE_CONTACTS;android.permission.WRITE_EXTERNAL_STORAGE;android.permission.ACCESS_NETWORK_STATE;android.permission.ACCOUNT_MANAGER;android.permission.GET_ACCOUNTS;android.permission.MANAGE_ACCOUNTS;android.permission.BROADCAST_STICKY
</AndroidPermissions>
<CorePlugins>
org.apache.cordova.core.device;org.apache.cordova.core.AudioHandler;org.apache.cordova.core.battery-status;org.apache.cordova.core.camera;org.apache.cordova.core.console;org.apache.cordova.core.contacts;org.apache.cordova.core.device;org.apache.cordova.core.device-motion;org.apache.cordova.core.device-orientation;org.apache.cordova.core.dialogs;org.apache.cordova.core.file;org.apache.cordova.core.file-transfer;org.apache.cordova.core.geolocation;org.apache.cordova.core.globalization;org.apache.cordova.core.inappbrowser;org.apache.cordova.core.media-capture;org.apache.cordova.core.network-information;org.apache.cordova.core.splashscreen;org.apache.cordova.core.vibration
</CorePlugins>
<DeviceOrientations>Portrait;Landscape</DeviceOrientations>
<iOSStatusBarStyle>Default</iOSStatusBarStyle>
<AndroidHardwareAcceleration>false</AndroidHardwareAcceleration>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
</PropertyGroup>
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<OutputPath>bin\Debug\$(DevicePlatform)\</OutputPath>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<OutputPath>bin\Release\$(DevicePlatform)\</OutputPath>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
</PropertyGroup>
<ItemGroup>
<Resource Include="App_Resources\iOS\icon.png"/>
<Resource Include="App_Resources\iOS\icon@2x.png"/>
<Resource Include="App_Resources\iOS\icon-72.png"/>
<Resource Include="App_Resources\iOS\icon-72@2x.png"/>
<Resource Include="App_Resources\iOS\Icon-Small.png"/>
<Resource Include="App_Resources\iOS\Icon-Small@2x.png"/>
<Resource Include="App_Resources\iOS\Icon-Small-50.png"/>
<Resource Include="App_Resources\iOS\Icon-Small-50@2x.png"/>
<Resource Include="App_Resources\iOS\Default.png"/>
<Resource Include="App_Resources\iOS\Default@2x.png"/>
<Resource Include="App_Resources\iOS\Default-568h@2x.png"/>
<Resource Include="App_Resources\iOS\Default-Portrait.png"/>
<Resource Include="App_Resources\iOS\Default-Landscape.png"/>
<Resource Include="App_Resources\iOS\Default-Portrait@2x.png"/>
<Resource Include="App_Resources\iOS\Default-Landscape@2x.png"/>
<Resource Include="App_Resources\iOS\icon-40.png"/>
<Resource Include="App_Resources\iOS\icon-40@2x.png"/>
<Resource Include="App_Resources\iOS\icon-60.png"/>
<Resource Include="App_Resources\iOS\icon-60@2x.png"/>
<Resource Include="App_Resources\iOS\icon-76.png"/>
<Resource Include="App_Resources\iOS\icon-76@2x.png"/>
</ItemGroup>
<ItemGroup>
<Resource Include="App_Resources\Android\drawable-hdpi\icon.png"/>
<Resource Include="App_Resources\Android\drawable-mdpi\icon.png"/>
<Resource Include="App_Resources\Android\drawable-ldpi\icon.png"/>
<Resource Include="App_Resources\Android\drawable-nodpi\splashscreen.9.png"/>
</ItemGroup>
<ItemGroup>
<Content Include="index.html"/>
<Content Include="kendo\js\jquery.min.js"/>
<Content Include="kendo\js\kendo.mobile.min.js"/>
<Content Include="kendo\styles\images\back.png"/>
<Content Include="kendo\styles\images\KendoUI.ttf"/>
<Content Include="kendo\styles\images\KendoUI.woff"/>
<Content Include="kendo\styles\images\wp8_icons.png"/>
<Content Include="kendo\styles\images\wp8_inverseicons.png"/>
<Content Include="kendo\styles\kendo.mobile.all.min.css"/>
<Content Include="scripts\app.js"/>
<Content Include="scripts\undescore.js"/>
<Content Include="scripts\machina.min.js"/>
<Content Include="scripts\postal.min.js"/>
<Content Include="styles\images\icenium.png"/>
<Content Include="styles\images\icenium2x.png"/>
<Content Include="styles\images\logo.png"/>
<Content Include="styles\images\logo2x.png"/>
<Content Include="styles\main.css"/>
<Content Include="cordova.ios.js">
<Platform>iOS</Platform>
<TargetName>cordova.js</TargetName>
</Content>
<Content Include="cordova.android.js">
<Platform>Android</Platform>
<TargetName>cordova.js</TargetName>
</Content>
</ItemGroup>
<Import Project="$(IceniumTargets)Build.targets"/>
<!--
TODO: Uncomment this code if you need to customize the build process.
<Target Name="BeforeBuild">
<Message Text="TODO: Implement BeforeBuild" Importance="high" />
</Target>
<Target Name="AfterBuild">
<Message Text="TODO: Implement AfterBuild" Importance="high" />
</Target>
-->
</Project>
So far we've only edited the .vstemplate and .iceproj files - and that's fine. However, there are files inside our directory structure that need to be removed. That being the case, the next steps are:
We don't need these file anymore, and wouldn't want them showing up in the directory structure of a new project on the file system.
Since we've started with the Icenium Kendo UI Mobile project template as a base, this means our index.html
, main.css
and app.js
files have sample app boilerplate that we need to remove.
Let's take the index.html
file first. I removed all the sample-app-related boilerplate, so that it contains only this:
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8" />
<link href="kendo/styles/kendo.mobile.all.min.css" rel="stylesheet" />
<link href="styles/main.css" rel="stylesheet" />
</head>
<body>
<!--Layout-->
<div data-role="layout" data-id="tabstrip-layout" >
<!--Header-->
<div data-role="header">
</div>
<!--Footer-->
<div data-role="footer">
</div>
</div>
<script src="cordova.js"></script>
<script src="kendo/js/jquery.min.js"></script>
<script src="kendo/js/kendo.mobile.min.js"></script>
<script src="scripts/underscore.min.js"></script>
<script src="scripts/machina.js"></script>
<script src="scripts/postal.js"></script>
<script src="scripts/app.js"></script>
</body>
</html>
You'll notice that I left part of the "layout" markup in place - since I anticipate using it in nearly all of my apps. I'll just have to include the specifics of what goes in the header and footer (which, understandably, will vary per application). I also moved the script include to the end of the body tag, and added script tags for underscore, machina.js and postal.js.
Next, with our app.js
file, I removed all sample-app-related boilerplate, so it contains the following:
(function (global) { var app = global.app = global.app || {}; document.addEventListener("deviceready", function () { app.application = new kendo.mobile.Application(document.body, { layout: "tabstrip-layout" }); }, false); })(window);
For my template, I'd like for an empty stylesheet to be included, so I removed all the CSS from styles/main.css, but left the file itself in place.
We need to be sure and physically add underscore, machina and postal to our scripts directory, or else our project template would be embarassingly broken:
OK, great! At this point we've updated the .vstemplate and .iceproj files, we've removed all the assets we don't need, we've modified the existing index.html
, main.css
and app.js
files, and we've added our new dependencies to the scripts directory. All we need to do now is move this folder structure somewhere it can be recognized by Visual Studio. We can copy/move the folder containing our project template to the %USERPROFILE%\My Documents\Visual Studio 2012\Templates\ProjectTemplates
folder. Once you've done this, close Visual Studio if it happens to be open, and then re-lauch it. Once Visual Studio is open, go to File -> New Project, and you'll see our new project template listed under the "Icenium" project template list:
You might find that others are interested in project templates you've created. There's a much better way to share them than simply giving someone a zip of your project template directory and sending it to them (though that's certainly a valid way to do it as well). If you follow the steps of this article, you'll get an idea of how you can create a Visual Studio Extension project, and ultimately create an extension that installs your custom project template. You can deploy that extension to the Visual Studio Gallery site, giving others access to your custom template(s). (Be sure you have the Visual Studio 2012 SDK installed before you start down this path.)
Jim Cowart is an architect, developer, open source author, and overall web/hybrid mobile development geek. He is an active speaker and writer, with a passion for elevating developer knowledge of patterns and helpful frameworks.