Windows to Web

AngularJS for XAML developers

Jeff Yates / blog.somewhatabstract.com / @jefftunes

13 years

Windows

Web

Application

Silverlight


<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
             x:Class="HelloWorld.App" />

namespace HelloWorld
{
  public partial class App : System.Windows.Application
  {
    public App()
    {
      this.Startup +=
        (sender, e) => this.RootVisual = new TextBlock() { FontSize = 24, Text = "Hello World" };
      InitializeComponent();
    }
  }
}

<object data="data:application/x-silverlight-2," width="100%" type="application/x-silverlight-2">
    <param name="source" value="Bin/Debug/HelloWorld.xap"/>
</object>

Angular


<html ng-app>
    <head>
        <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.6/angular.min.js"></script>
    </head>
    <body>
        Hello World
    </body>
</html>

<UserControl x:Class="ShakespeareanInsultGenerator.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:ShakespeareanInsultGenerator"
    mc:Ignorable="d"
    FontSize="36"
    FontFamily="Arial"
    d:DesignHeight="150" d:DesignWidth="400">
    <UserControl.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="Foreground" Value="#EEEEEE" />
            <Setter Property="VerticalAlignment" Value="Center"/>
            <Setter Property="TextAlignment" Value="Right" />
            <Setter Property="Margin" Value="3" />
            <Setter Property="FontFamily" Value="Lato"/>
            <Setter Property="FontStyle" Value="Italic" />
        </Style>
        <Style TargetType="ComboBox">
            <Setter Property="Margin" Value="3"/>
        </Style>
        <Style TargetType="Button">
            <Setter Property="Margin" Value="3"/>
            <Setter Property="VerticalAlignment" Value="Top"/>
            <Setter Property="FontWeight" Value="Bold"/>
        </Style>
        <Style TargetType="RichTextBox">
            <Setter Property="Margin" Value="3"/>
            <Setter Property="IsReadOnly" Value="True"/>
            <Setter Property="TextAlignment" Value="Left"/>
            <Setter Property="FontWeight" Value="Bold"/>
            <Setter Property="FontSize" Value="54"/>
            <Setter Property="FontFamily" Value="Courier New" />
        </Style>
        <Style TargetType="TextBlock" x:Key="HeaderStyle">
            <Setter Property="Foreground" Value="#EEEEEE" />
            <Setter Property="FontSize" Value="60"/>
            <Setter Property="Margin" Value="3"/>
            <Setter Property="FontFamily" Value="Impact"/>
            <Setter Property="TextAlignment" Value="Center"/>
        </Style>
        <local:InsultModel x:Key="Model" />
    </UserControl.Resources>
    <Grid x:Name="LayoutRoot" Background="#203030" DataContext="{Binding Source={StaticResource Model}}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>
        <TextBlock Style="{StaticResource HeaderStyle}" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="0">SHAKESPEAREAN INSULT GENERATOR</TextBlock>
        <TextBlock Text="Adjective 1:" Grid.Column="0" Grid.Row="1" />
        <ComboBox Grid.Column="1" Grid.Row="1" ItemsSource="{Binding FirstAdjectiveList}" SelectedValue="{Binding FirstAdjective, Mode=TwoWay}" />
        <TextBlock Text="Adjective 2:" Grid.Column="0" Grid.Row="2" />
        <ComboBox Grid.Column="1" Grid.Row="2" ItemsSource="{Binding SecondAdjectiveList}" SelectedValue="{Binding SecondAdjective, Mode=TwoWay}" />
        <TextBlock Text="Noun:" Grid.Column="0" Grid.Row="3" />
        <ComboBox Grid.Column="1" Grid.Row="3" ItemsSource="{Binding NounList}" SelectedValue="{Binding Noun, Mode=TwoWay}" />
        <Button Grid.Column="0" Grid.Row="4" Content="Randomize" Click="OnClickRandomize" />
        <RichTextBox  Grid.Column="1" Grid.Row="4">
            <RichTextBox.Blocks>
                <Paragraph>
                    <Span>
                        <Run Text="Thou" />
                        <Run Text="{Binding FirstAdjective}" />
                        <Run Text="{Binding SecondAdjective}" />
                        <Run Text="{Binding Noun, StringFormat='{0}!'}" />
                    </Span>
                </Paragraph>
            </RichTextBox.Blocks>
        </RichTextBox>
    </Grid>
</UserControl>

public partial class MainPage : UserControl
{
    public MainPage()
    {
        InitializeComponent();
    }
    private void OnClickRandomize( object sender, RoutedEventArgs e )
    {
        ((InsultModel)Resources["Model"]).Randomize();
    }
}

public class InsultModel : INotifyPropertyChanged
{
    public InsultModel()
    {
        _firstAdjectiveList = Words.Adjectives.ToList().AsReadOnly();
        _secondAdjectiveList = Words.HyphenatedAdjectives.ToList().AsReadOnly();
        _nounList = Words.Nouns.ToList().AsReadOnly();
        Randomize();
    }
    public void Randomize()
    {
        FirstAdjective = _firstAdjectiveList.Random();
        SecondAdjective = _secondAdjectiveList.Random();
        Noun = _nounList.Random();
    }
    public string FirstAdjective
    {
        get { return _firstAdjective; }
        set
        {
            if ( _firstAdjective != value )
            {
                _firstAdjective = value;
                OnPropertyChanged( "FirstAdjective" );
            }
        }
    }
    public string SecondAdjective
    {
        get { return _secondAdjective; }
        set
        {
            if ( _secondAdjective != value )
            {
                _secondAdjective = value;
                OnPropertyChanged( "SecondAdjective" );
            }
        }
    }
    
    public string Noun
    {
        get { return _noun; }
        set
        {
            if ( _noun != value )
            {
                _noun = value;
                OnPropertyChanged( "Noun" );
            }
        }
    }
    public IEnumerable<string> FirstAdjectiveList { get { return _firstAdjectiveList; } }
    public IEnumerable<string> SecondAdjectiveList { get { return _secondAdjectiveList; } }
    public IEnumerable<string> NounList { get { return _nounList; } }
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged( string propertyName )
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if ( handler != null ) { handler( this, new PropertyChangedEventArgs( propertyName ) ); }
    }
    private string _firstAdjective;
    private string _secondAdjective;
    private string _noun;
    private readonly IEnumerable<string> _firstAdjectiveList;
    private readonly IEnumerable<string> _secondAdjectiveList;
    private readonly IEnumerable<string> _nounList;
}

Data Binding

Hello {{textStuff}}
<StackPanel x:Name="LayoutRoot">
  <TextBox x:Name="textStuff" />
  <TextBlock Text="{Binding ElementName=textStuff, Path=Text, StringFormat='Hello {0}'}" />
</StackPanel>
<div ng-app>
  <input ng-model="textStuff" />
  <div>Hello {{textStuff}}</div>
</div>
<StackPanel x:Name="LayoutRoot">
  <TextBox x:Name="textStuff" />
  <TextBlock Text="{Binding ElementName=textStuff, Path=Text, StringFormat='Hello {0}'}" />
  <TextBox Text="{Binding ElementName=textStuff, Path=Text}" />
</StackPanel>
<div ng-app>
  <input ng-model="textStuff" />
  <div>Hello {{textStuff}}</div>
  <input ng-model="textStuff" />
</div>

Code-behind

<UserControl x:Class="HelloWorld.MainPage">
    xmlns:local="clr-namespace:HelloWorld"
...
              
<div ng-app='myApp' ng-controller='myController'>
  ...
namespace HelloWorld
{
  public partial class MainPage : UserControl
  {
    ...
              
var myApp = angular.module('myApp', []);
myApp.controller('myController', function ($scope) {
  ...
<div ng-app='myApp' ng-controller='myController'>
  <input ng-model="textStuff" />
  <div>Hello {{textStuff}}</div>
  <input ng-model="textStuff" />
</div>
var myApp = angular.module('myApp', []);
myApp.controller('myController', function ($scope) {
  $scope.textStuff = 'World';
});

Value Converters

{{stringToReverse | reverse}}
<StackPanel x:Name="LayoutRoot">
  <StackPanel.Resources>
    <myApp:ReverseStringConverter x:Key="ReverseStringConverter" />
  </StackPanel.Resources>
  <TextBox x:Name="textStuff" />
  <TextBlock Text="{Binding ElementName=textStuff, Path=Text, Converter={StaticResource ReverseStringConverter}}" />
</StackPanel>
<div ng-app>
  <input ng-model="textStuff" />
  <div>{{textStuff | reverse}}</div>
</div>
public class ReverseStringConverter : IValueConverter
{
  public object Convert( object value, Type targetType, object parameter, CultureInfo culture )
  {
    return new String(value.ToString().Reverse());
  }
  public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture )
  {
    throw new NotImplementedException();
  }
}
module('myApp').filter('reverse', function() {
  return function (value){
    return value.split("").reverse().join("");
  };
});
{{stringToReverse | reverse | uppercase}}
<div ng-app>
  <input ng-model="textStuff" />
  <div>{{textStuff | reverse | uppercase}}</div>
</div>

Data Binding Collections

<ItemsControl x:Name="List">
  <ItemsControl.ItemTemplate>
    <DataTemplate>
      <TextBlock Text="{Binding}"></TextBlock>
    </DataTemplate>
  </ItemsControl.ItemTemplate>
</ItemsControl>
List.ItemsSource = new[] { "Leprechaun", "Unicorn", "Bacon" }

<p ng-repeat="name in names">{{name}}</p>
$scope.names = [ 'Leprechaun', 'Unicorn', 'Bacon'];

Sorting and filtering

{{name}}

var collectionView = new CollectionViewSource();
collectionView.Source = new[] { "Leprechaun", "Unicorn", "Bacon" };
collectionView.View.SortDescriptions.Add(
  new SortDescription { Direction = ListSortDirection.Descending } );
collectionView.View.Filter =
  o => String.IsNullOrEmpty( CriteriaTextBox.Text )
      || ( (string)o ).Contains( CriteriaTextBox.Text );
    
List.ItemsSource = collectionView.View;
CriteriaTextBox.TextChanged +=
  (sender, args) => ((ICollectionView)List.ItemsSource).Refresh();
<div ng-controller="myController">
  <input ng-model="criteria"/>
  <p ng-repeat="name in names | filter:criteria | orderBy:'-toString()'">{{name}}</p>
</div>

ComboBox

<ComboBox ItemsSource="{Binding Words}" SelectedValue="{Binding SelectedWord, Mode=TwoWay}" />
<select ng-options="word for word in words" ng-model="selectedWord"></select>

Style

SURPRISE!
No surprise :(
<StackPanel x:Name="LayoutRoot" Background="White">
  <CheckBox
    IsChecked="{Binding IsSurprise, Mode=TwoWay}">Toggle surprise</CheckBox>
  <TextBlock Text="SURPRISE!"
    Visibility="{Binding IsSurprise, Converter={StaticResource VisibilityConverter}}" />
</StackPanel>
<div>
  <input type="checkbox" ng-model="isShowingSurprise" name="showSurpriseCheck" />
  <label for="showSurpriseCheck">Toggle surprise</label>
  <div ng-show="isShowingSurprise">SURPRISE!</div>
</div>
Some status message
<CheckBox IsChecked="{Binding HasError, Mode=TwoWay}">Has Error</CheckBox>
<TextBlock Text="Some status message" FontStyle="{Binding HasError, Converter={StaticResource FontStyleConverter}}" />
<input type="checkbox" ng-model="hasError" name="isErrorRadio" />
<label for="isErrorRadio">Has Error</label>
<div ng-style="{'font-style': hasError ? 'italic' : 'normal'}">Some status message</div>
Totally different status message
<VisualStateManager.VisualStateGroups>
  <VisualStateGroup x:Name="CommonStates">
    <VisualState x:Name="Error">
      <Storyboard>
        <ColorAnimation To="{StaticResource ErrorColor}" Duration="0:00:00" Storyboard.TargetName="MessageTextBlock" Storyboard.TargetProperty="(TextBlock.Foreground).(SolidColorBrush.Color)" />
      </Storyboard>
    </VisualState>
    <VisualState x:Name="Normal">
      <Storyboard>
        <ColorAnimation To="{StaticResource OkColor}" Duration="0:00:00" Storyboard.TargetName="MessageTextBlock" Storyboard.TargetProperty="(TextBlock.Foreground).(SolidColorBrush.Color)" />
      </Storyboard>
    </VisualState>
  </VisualStateGroup>
</VisualStateManager.VisualStateGroups>
VisualStateManager.GoToState( this, "Error", true );
Totally different status message
.status-ok {
  color: green; }
.status-error {
  color: red; }
<div ng-class="{ 'status-error': hasError, 'status-ok': !hasError }">
  Totally different status message
</div>

Controls


<Style TargetType="myApp:StatusMessage">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="myApp:StatusMessage">
        <TextBlock Text="Our status message"/>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>
public class StatusMessage : Control
{
  public StatusMessage()
  {
    this.DefaultStyleKey = typeof(StatusMessage);
  }
}
module('myApp').directive('statusMessage', function() {
  return {
    template: '<div>Our status message</div>',
    restrict: 'E'
  };
});
<myApp:StatusMessage />
<status-message />

Dependency Properties

<ControlTemplate TargetType="myApp:StatusMessage">
  <TextBlock Text="{TemplateBinding Message}" />
</ControlTemplate>
public static readonly DependencyProperty MessageProperty =
  DependencyProperty.Register( "Message",
                 typeof ( string ),
                 typeof ( StatusMessage ),
                 new PropertyMetadata( default( string ) ) );
public string Message
{
  get { return (string) GetValue( MessageProperty ); }
  set { SetValue( MessageProperty, value ); }
}
codemash2014.directive('statusMessage', function() {
  return {
    template: '<div>{{message}}</div>',
    restrict: 'E',
    scope: { message: '@' }
  };
});
<myApp:StatusMessage Message="Now THIS is a status message. Exciting!" />
<status-message message="Now THIS is a status message. Exciting!" />

codemash2014.directive('labelledCombo', function() {
        return {
            template: '<div><div class="label">{{title}}</div><select ng-model="selectedItem" ng-options="item for item in items"></select></div>',
            restrict: 'E',
            replace: true,
            scope: {
                selectedItem: '=',
                items: '=',
                title: '@'
            }
        };
    });
<labelled-combo title="Adjective 1:" items="firstAdjectives" selected-item="firstAdjective"></labelled-combo>

Putting It Together


<UserControl x:Class="ShakespeareanInsultGenerator.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:ShakespeareanInsultGenerator"
    mc:Ignorable="d"
    FontSize="36"
    FontFamily="Arial"
    d:DesignHeight="150" d:DesignWidth="400">
    <UserControl.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="Foreground" Value="#EEEEEE" />
            <Setter Property="VerticalAlignment" Value="Center"/>
            <Setter Property="TextAlignment" Value="Right" />
            <Setter Property="Margin" Value="3" />
            <Setter Property="FontFamily" Value="Lato"/>
            <Setter Property="FontStyle" Value="Italic" />
        </Style>
        <Style TargetType="ComboBox">
            <Setter Property="Margin" Value="3"/>
        </Style>
        <Style TargetType="Button">
            <Setter Property="Margin" Value="3"/>
            <Setter Property="VerticalAlignment" Value="Top"/>
            <Setter Property="FontWeight" Value="Bold"/>
        </Style>
        <Style TargetType="RichTextBox">
            <Setter Property="Margin" Value="3"/>
            <Setter Property="IsReadOnly" Value="True"/>
            <Setter Property="TextAlignment" Value="Left"/>
            <Setter Property="FontWeight" Value="Bold"/>
            <Setter Property="FontSize" Value="54"/>
            <Setter Property="FontFamily" Value="Courier New" />
        </Style>
        <Style TargetType="TextBlock" x:Key="HeaderStyle">
            <Setter Property="Foreground" Value="#EEEEEE" />
            <Setter Property="FontSize" Value="60"/>
            <Setter Property="Margin" Value="3"/>
            <Setter Property="FontFamily" Value="Impact"/>
            <Setter Property="TextAlignment" Value="Center"/>
        </Style>
        <local:InsultModel x:Key="Model" />
    </UserControl.Resources>
    <Grid x:Name="LayoutRoot" Background="#203030" DataContext="{Binding Source={StaticResource Model}}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>
        <TextBlock Style="{StaticResource HeaderStyle}" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="0">SHAKESPEAREAN INSULT GENERATOR</TextBlock>
        <TextBlock Text="Adjective 1:" Grid.Column="0" Grid.Row="1" />
        <ComboBox Grid.Column="1" Grid.Row="1" ItemsSource="{Binding FirstAdjectiveList}" SelectedValue="{Binding FirstAdjective, Mode=TwoWay}" />
        <TextBlock Text="Adjective 2:" Grid.Column="0" Grid.Row="2" />
        <ComboBox Grid.Column="1" Grid.Row="2" ItemsSource="{Binding SecondAdjectiveList}" SelectedValue="{Binding SecondAdjective, Mode=TwoWay}" />
        <TextBlock Text="Noun:" Grid.Column="0" Grid.Row="3" />
        <ComboBox Grid.Column="1" Grid.Row="3" ItemsSource="{Binding NounList}" SelectedValue="{Binding Noun, Mode=TwoWay}" />
        <Button Grid.Column="0" Grid.Row="4" Content="Randomize" Click="OnClickRandomize" />
        <RichTextBox  Grid.Column="1" Grid.Row="4">
            <RichTextBox.Blocks>
                <Paragraph>
                    <Span>
                        <Run Text="Thou" />
                        <Run Text="{Binding FirstAdjective}" />
                        <Run Text="{Binding SecondAdjective}" />
                        <Run Text="{Binding Noun, StringFormat='{0}!'}" />
                    </Span>
                </Paragraph>
            </RichTextBox.Blocks>
        </RichTextBox>
    </Grid>
</UserControl>

codemash2014.directive('insultGenerator', function() {
    return {
        controller: 'insultGeneratorCtrl',
        replace: true,
        template:
            '<div class="insult-generator">'
            + '<h2>{{title | uppercase}}</h2>'
            + '<labelled-combo title="Adjective 1:" items="firstAdjectives" selected-item="firstAdjective"></labelled-combo>'
            + '<labelled-combo title="Adjective 2:" items="secondAdjectives" selected-item="secondAdjective"></labelled-combo>'
            + '<labelled-combo title="Noun:" items="nouns" selected-item="noun"></labelled-combo>'
            + '<div>'
                + '<button ng-click="randomize()">Randomize</button>'
                + '<div class="insult-container"><textarea class="insult" readonly rows="2">Thou {{firstAdjective}} {{secondAdjective}} {{noun}}!</textarea></div>'
            + '</div>'
            + '</div>',
        restrict: 'E',
        scope: { title: '@' }
    };
});

public partial class MainPage : UserControl
{
    public MainPage()
    {
        InitializeComponent();
    }
    private void OnClickRandomize( object sender, RoutedEventArgs e )
    {
        ((InsultModel)Resources["Model"]).Randomize();
    }
}

public class InsultModel : INotifyPropertyChanged
{
    public InsultModel()
    {
        _firstAdjectiveList = Words.Adjectives.ToList().AsReadOnly();
        _secondAdjectiveList = Words.HyphenatedAdjectives.ToList().AsReadOnly();
        _nounList = Words.Nouns.ToList().AsReadOnly();
        Randomize();
    }
    public void Randomize()
    {
        FirstAdjective = _firstAdjectiveList.Random();
        SecondAdjective = _secondAdjectiveList.Random();
        Noun = _nounList.Random();
    }
    public string FirstAdjective
    {
        get { return _firstAdjective; }
        set
        {
            if ( _firstAdjective != value )
            {
                _firstAdjective = value;
                OnPropertyChanged( "FirstAdjective" );
            }
        }
    }
    public string SecondAdjective
    {
        get { return _secondAdjective; }
        set
        {
            if ( _secondAdjective != value )
            {
                _secondAdjective = value;
                OnPropertyChanged( "SecondAdjective" );
            }
        }
    }
    
    public string Noun
    {
        get { return _noun; }
        set
        {
            if ( _noun != value )
            {
                _noun = value;
                OnPropertyChanged( "Noun" );
            }
        }
    }
    public IEnumerable<string> FirstAdjectiveList { get { return _firstAdjectiveList; } }
    public IEnumerable<string> SecondAdjectiveList { get { return _secondAdjectiveList; } }
    public IEnumerable<string> NounList { get { return _nounList; } }
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged( string propertyName )
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if ( handler != null ) { handler( this, new PropertyChangedEventArgs( propertyName ) ); }
    }
    private string _firstAdjective;
    private string _secondAdjective;
    private string _noun;
    private readonly IEnumerable<string> _firstAdjectiveList;
    private readonly IEnumerable<string> _secondAdjectiveList;
    private readonly IEnumerable<string> _nounList;
}

codemash2014.controller('insultGeneratorCtrl', function($scope, insultWords) {
    $scope.firstAdjectives = insultWords.firstAdjectives;
    $scope.secondAdjectives = insultWords.secondAdjectives;
    $scope.nouns = insultWords.nouns;
    $scope.randomize = function() {
        $scope.firstAdjective = getRandomElement($scope.firstAdjectives);
        $scope.secondAdjective = getRandomElement($scope.secondAdjectives);
        $scope.noun = getRandomElement($scope.nouns);
    };
    function getRandomIndex(limit) {
        return Math.round(limit * Math.random() + 0.5);
    }
    function getRandomElement(array) {
        var index = getRandomIndex(array.length - 1);
        return array[index];
    }
    $scope.randomize();
});

codemash2014.value('insultWords', {
    firstAdjectives: [
        "artless",
        "bawdy",
        ...],
    secondAdjectives: [
        "base-court",
        "bat-fowling",
        ...],
    nouns: [
        "apple-john",
        "baggage",
        ...]
});

<insult-generator title='Shakespearean Insult Generator' />

Resources

Questions