Introduction
Settings pages are something almost every mobile phone application has to provide. I am experimenting with the Windows Phone 8 SDK and found several good articles explaining how to
create settings pages for Windows Phone (the one from the Dev Center is quite helpful). I soon got tired of this approach as the application I am experimenting with has tons of user settings. Therefore, I was wondering whether these lines of codes could be generated using some code generation tool such as T4. The idea is to provide an XML file defining the list of settings and some T4 that generates the Views and Models.
For example, something like this:
="1.0" ="utf-8"
<SettingsScreen Type="Pivot" Title="SETTINGS"
Namespace="JuiceOntour.T4Settings.Settings">
<SettingsCategory Key="NotifyCategory" Title="notifications"
Summary="define how you want to be notified.">
<ToggleSwitchItem Key="DoNotify" Title="Notification"
Summary="You will be notified when something happens."
DefaultValue="true"/>
<SmallTextBoxItem Key="NotificationTimeout"
Title="Timeout (ms)" Summary="" DefaultValue="500"
Dependency="DoNotify" DependencyIndent="true"/>
<ToggleSwitchItem Key="KeepLog" Title="Keep log"
Summary="The app will keep a log of all notifications."
DefaultValue="false"/>
</SettingsCategory>
<SettingsCategory Key="CloudCategory" Title="cloud"
Summary="The app is able to connect to the cloud.">
<ToggleSwitchItem Key="DoCloud"
Title="Connect to the cloud" Summary=""
DefaultValue="false"/>
<ToggleSwitchItem Key="Backup"
Title="Backup to the cloud" Summary=""
DefaultValue="false" Dependency="DoCloud"
DependencyIndent="false"/>
</SettingsCategory>
</SettingsScreen>
Will end up in something like this:
Background
This solution makes extensive use of Oleg Sych’s article on how to generate multiple outputs from single T4 template (http://www.olegsych.com/2008/03/how-to-generate-multiple-outputs-from-single-t4-template/).
Using the Code
The code is mainly composed of a set of T4s: one per output file. Just add the provided code into your Windows Phone solution and let T4 do its wonders.
Take care to remove the Custom Tool property of every T4 but Settings.tt
in order to avoid unnecessary code generation.
SettingsScreen Node
The SettingsScreen node has the following mandatory attributes:
Namespace
: identifies the name space of the generated code
Type
: identifies the type of the settings screen (in the attached code Pivot and SubPages are supported)
Title
: The title of the screen. Note that binding is supported.
SettingsCategory node
The SettingsCategory
node has the following mandatory attributes:
Key
: identifies the category
Title
: The title of the category. Note that binding is supported.
Summary
: The summary of the category. Note that binding is supported.
Settings Items
The following settings items are supported by the attached code:
ToggelSwitchItem
(in the Windows Phone Toolkit)
TextBoxItem
SmallTextBoxItem
(same as textBox but holds on one line)
ListItem
(in the Windows Phone Toolkit)
The common and mandatory attributes are as follows:
Key
: identifies the item
Title
: The title of the item. Note that binding is supported.
Summary
: The summary of the item. Note that binding is supported.
DefaultValue
: The default value.
Any item can take the following non-mandatory attributes:
Dependency
: Takes as a value the key of any ToggleSwitchItem
. A dependent Item is disabled when the specified ToggleSwitch is OFF.
DependencyIndent
: If the value is set to true
, then the item is indented.
The ListItem
takes an additional mandatory attribute: ItemsSource
. Again, binding is supported.
Digging Deeper
The main template is the Settings.tt
doing the orchestration. The XML file is parsed, its content stored and made accessible via remoting CallContext
. Finally the different templates are processed and their output files generated.
...
XDocument xmlDoc = ParseXml("AppSettings.xml");
CallContext.SetData("T4Settings.Parameter", xmlDoc);
ProcessTemplate("AppSettingsModel.tt", "AppSettings.cs");
if(GetSettingsScreen(xmlDoc).Attribute(TYPE_ATTRIBUTE).Value.Equals(PIVOT_SCREEN)){
ProcessTemplate("PivotScreen.tt", "SettingsMainPage.xaml");
ProcessTemplate("PivotScreenCS.tt", "SettingsMainPage.xaml.cs");
}
...
There’s another important template providing access to the elements within the XML file: XDocumentParser.tt
. This template also provides some commonly used constants such as node and attribute names.
...
string NAMESPACE_ATTRIBUTE = "Namespace";
string KEY_ATTRIBUTE = "Key";
string TITLE_ATTRIBUTE = "Title";
string SUMMARY_ATTRIBUTE = "Summary";
...
string TOGGLESWITCH_ITEM = "ToggleSwitchItem";
string SMALLTEXTBOX_ITEM = "SmallTextBoxItem";
...
XElement GetSettingsScreen(XDocument doc)
{
if(doc == null) return null;
return doc.Descendants("SettingsScreen").First();
}
IEnumerable<XElement> GetCatregories(XDocument doc)
{
if(doc == null) return null;
return doc.Descendants("SettingsCategory");
}
IEnumerable<XElement> GetSettingsItems(XElement category, XDocument doc)
{
if(doc == null) return null;
return doc.Descendants("SettingsCategory").Where(c =>
c.Attribute(KEY_ATTRIBUTE).Value.Equals
(category.Attribute(KEY_ATTRIBUTE).Value)).Descendants();
}
IEnumerable<XElement> GetAllSettingsItems(XDocument doc)
{
if(doc == null) return null;
var categories = GetCatregories(doc);
List<XElement> list = new List<XElement>();
foreach(var category in categories)
{
list.AddRange(category.Descendants());
}
return list;
}
...
These templates are doing the plumbing between the XML file listing the settings and the generated code. Once these templates are in place, the creating of the remaining templates is as simple as basic T4 coding.
The AppSettingsModel.tt
template is generating the ViewModel as described in the Dev Center Article mentioned above. It mainly loops over all the items within the XML file and generates the code. Note that the generated class is partial
which allows you to add your custom properties to the ViewModel.
...
<#@ include file="XDocumentParser.tt" #>
using System.IO.IsolatedStorage;
namespace <#= GetSettingsScreen(Parameter).Attribute(NAMESPACE_ATTRIBUTE).Value #>
{
public partial class AppSettings
{
private readonly IsolatedStorageSettings _settings;
#region key name and default value of the application settings
<#
IEnumerable<XElement> items = GetAllSettingsItems(Parameter);
foreach (var item in items) {
#>
private const string <#= item.Attribute(KEY_ATTRIBUTE).Value #>Key =
"<#= GetSettingsScreen(Parameter).Attribute
(NAMESPACE_ATTRIBUTE).Value #>.<#= item.Attribute(KEY_ATTRIBUTE).Value #>";
private const <#= GetItemType(item) #>
<#= item.Attribute(KEY_ATTRIBUTE).Value #>DefaultValue =
<# if(GetItemType(item).Equals("string"))
{ #>"<# } #><#= item.Attribute
(DEFAULTVALUE_ATTRIBUTE).Value #><# if(GetItemType
(item).Equals("string")) { #>"<# } #>;
<# } #>
#endregion
...
#region public members
<#
foreach (var item in items) {
#>
public <#= GetItemType(item) #> <#= item.Attribute(KEY_ATTRIBUTE).Value #>
{
get
{
return GetValueOrDefault(<#= item.Attribute(KEY_ATTRIBUTE).Value #>Key,
<#= item.Attribute(KEY_ATTRIBUTE).Value #>DefaultValue);
}
set
{
if (AddOrUpdateValue(<#= item.Attribute(KEY_ATTRIBUTE).Value #>Key, value))
{
Save();
}
}
}
<# } #>
#endregion
...
<#+
XDocument Parameter
{
get{ return (XDocument)CallContext.GetData("T4Settings.Parameter"); }
}
#>
The view and its code behind are rather straightforward as well. The code behind sets the generated ViewModel
as its DataContext
:
...
<#@ include file="XDocumentParser.tt" #>
namespace <#= GetSettingsScreen(Parameter).Attribute(NAMESPACE_ATTRIBUTE).Value #>
{
public partial class SettingsMainPage
{
#region class construction
public SettingsMainPage()
{
InitializeComponent();
DataContext = new AppSettings();
}
#endregion
}
}
...
The template for the XAML file loops over the categories and generates pivot items (or tapable list items if sub pages are used) and lists the settings of each category according to its type:
...
<#@ include file="XDocumentParser.tt" #>
...
<Grid x:Name="LayoutRoot" Background="Transparent">
<phone:Pivot Title="<#= GetSettingsScreen
(Parameter).Attribute(TITLE_ATTRIBUTE).Value #>">
<#
IEnumerable<XElement> categories = GetCatregories(Parameter);
foreach (var category in categories) {
#>
<phone:PivotItem Header="<#=
category.Attribute(TITLE_ATTRIBUTE).Value #>">
<StackPanel>
<TextBlock Text="<#= category.Attribute
(SUMMARY_ATTRIBUTE).Value #>" Margin="15,0,0,10"
TextWrapping="Wrap"/>
<#
IEnumerable<XElement> items = GetSettingsItems(category, Parameter);
foreach (var item in items) {
...
<# if(item.Name.ToString().Equals(TOGGLESWITCH_ITEM)) { #>
<Grid Margin="{StaticResource PhoneTouchTargetOverhang}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<toolkit:ToggleSwitch
x:Name="<#= item.Attribute(KEY_ATTRIBUTE).Value #>"
Grid.Row="0"
Header="<#= item.Attribute(TITLE_ATTRIBUTE).Value #>"
IsChecked="{Binding <#= item.Attribute
(KEY_ATTRIBUTE).Value #>, Mode=TwoWay}"/>
<TextBlock Grid.Row="1" Margin="10,-30,0,0"
Text="<#= item.Attribute(SUMMARY_ATTRIBUTE).Value #>"
FontSize="{StaticResource PhoneFontSizeSmall}"
TextWrapping="Wrap"/>
</Grid>
<# } #>
...
...
Conclusion
Please note that I did not polish the code to any extend (as I am only experimenting with Windows Phone). The provided solution is but an entry point to what could be achieved when digging deeper into T4. For example, one could include a SinglePage
screen type or other controls such as checkboxes or radio buttons. The ViewModel
is not Observable
which is something that could be added, too.