Akka.NET Dispatchers and User Interface Thread Access

Akka.NET actors typically run in their own (non-UI) threads. This means by default accessing UI objects (for example setting some text in a text box) will cause a thread access exception.

For example the following code shows an actor trying to set the text of the button. The actor is not running in the UI context but the Button is so we’re trying to cross threads which causes the exception shown below.

Screenshot of Visual Stuid showing thread access exception in debugger

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <Button Click="Button_OnClick">Say Hi</Button>
        <TextBox Name="GreetingTextBox"></TextBox>
    </StackPanel>
</Window>

public class HelloWorldActor : ReceiveActor
{
    public HelloWorldActor(TextBox textBox)
    {
        Receive<string>(s => textBox.Text = s);
    }
}
public partial class MainWindow : Window
{
    private ActorSystem _actorSystem;
    private IActorRef _actor;

    public MainWindow()
    {
        InitializeComponent();

        _actorSystem = ActorSystem.Create("MySystem");

        _actor = _actorSystem.ActorOf(Props.Create(() => new HelloWorldActor(GreetingTextBox)));
    }

    private void Button_OnClick(object sender, RoutedEventArgs e)
    {
        _actor.Tell("G'day");
    }
}

The thread on which an actor runs can be configured, either in code or in the app/web HOCON config.

We can modify the actor props in code as follows:

_actor = _actorSystem.ActorOf(Props.Create(() => new HelloWorldActor(GreetingTextBox))
                                   .WithDispatcher("akka.actor.synchronized-dispatcher"));

Now the HelloWorldActor will run on the UI thread and be able to access the text box as the following screenshot shows:

screenshot of WPF application running and updating the TextBox from another thread

 

MVVM and INotifyPropertyChanged

When using MVVM and a view-model, rather that passing the UI element to the actor, the view-model can instead be passed. The actor now updates the view-model which the textbox is databound to. In this situation the data-binding mechanism/INotifyPropertyChanged will take care of marshalling changes to the UI thread. This means that the actor no longer needs to be configured to run on the UI thread. The following code shows these changes:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <Button Click="Button_OnClick">Say Hi</Button>
        <TextBox Text="{Binding Greeting}"></TextBox>
    </StackPanel>
</Window>

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using Akka.Actor;

namespace WpfApplication1
{    
    public partial class MainWindow : Window
    {
        private ActorSystem _actorSystem;
        private IActorRef _actor;
        private ViewModel _viewModel ;

        public MainWindow()
        {
            InitializeComponent();

            _viewModel = new ViewModel();

            _actorSystem = ActorSystem.Create("MySystem");

            _actor = _actorSystem.ActorOf(Props.Create(() => new HelloWorldActor(_viewModel)));
            
            DataContext = _viewModel;
        }

        private void Button_OnClick(object sender, RoutedEventArgs e)
        {
            _actor.Tell("G'day");
        }
    }

    public class HelloWorldActor : ReceiveActor
    {
        private readonly ViewModel _viewModel;

        public HelloWorldActor(ViewModel viewModel)
        {
            _viewModel = viewModel;

            Receive<string>(s => _viewModel.Greeting = s);
        }
    }

    public class ViewModel : INotifyPropertyChanged
    {
        private string _greeting;

        public string Greeting
        {
            get { return _greeting; }
            set
            {
                _greeting = value;
                OnPropertyChanged();
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            var handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

You can read more about dispatchers in the Akka.NET docs and learn more about using Akka.NET with WPF in my Pluralsight course: Building Reactive Concurrent WPF Applications with Akka.NET.

EDIT: fixed up typo in source, changed to _actor.Tell("G'day");

SHARE:

Binding a Dynamically Created WPF Control’s ItemTemplate to a DataTamplate Defined in XAML

I was asked a question about this on Twitter, so thought I’d create a quick post about it.

The example below shows how to use a DataTemplate that is defined in XAML with a ListView control that is dynamically created in the code behind.

1 Create a New WPF Application

Create  WPF application project in Visual Studio.

2 Create The basic XAML

Replace the contents of the MainWindow XAML to the following:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <DataTemplate x:Key="DaysDataTemplate">
            <Grid Background="Cyan">
                <TextBlock Text="{Binding}"></TextBlock>
            </Grid>
        </DataTemplate>
    </Window.Resources>
    <Grid Name="RootContainer" />
</Window>

This defines a DataTemplate called DaysDataTemplate that simply binds the value and has a cyan background so we can see if it is being applied.

We also give the root container (Grid) a name RootContainer so we can reference it later in the code behind.

More...

SHARE:

MVVM Light Telling the View to play Storyboards

Sometimes you want to tell the view to play an animation (Storyboard). One simple way to do this is to define a StartStoryboardMessage class, populate this with the name of a Storyboard to play, then send it to the Messenger.

public class StartStoryboardMessage
{
    public string StoryboardName { getset; }
    public bool LoopForever { getset; }
}

In the viewmodel when you want to tell the view to play an animation:

Messenger.Default.Send(new StartStoryboardMessage { StoryboardName = "TimerFinAnimation",LoopForever=true });

The view (i.e. in the code-behind) registers for these messages:

Messenger.Default.Register<StartStoryboardMessage>(this, x => StartStoryboard(x.StoryboardName, x.LoopForever));

...

private void StartStoryboard(string storyboardName, bool loopForever)
{
    var storyboard = FindName(storyboardName) as Storyboard;
    if (storyboard != null)
    {
        if (loopForever) 
            storyboard.RepeatBehavior = RepeatBehavior.Forever;
        else
            storyboard.RepeatBehavior = new RepeatBehavior(1);
        storyboard.Begin();
    }
}

SHARE:

Notification Window "toast" in Silverlight 4

To create "toast" notifications (small popup messages that appear at the bottom right on Windows) the NotificationWindow class can be used.

NotificationWindow will only work in out-of-browser mode, if you try to show a notification when running in-browser a System.InvalidOperationException is thrown.

The default size of the toast is 400 x 100, the size can be altered by setting NotificationWindow's Width & Height, but the max size is 400 x 100;

Example 1 - Creating Some Simple Content

The code below shows a notification that contains a  TextBlock with the text "hello" and show it for 2 seconds:

var toast = new NotificationWindow();

// Create some content to show
var tb = new TextBlock();
tb.Text = "hello";

// Set content of the toast before we show it
toast.Content = tb;

// Show notification for 2 seconds
toast.Show(2000);

Example 2 - Using a UserControl as Content

This example shows how a UserControl defined elsewhere in XAML can be used as the content of the notification:

var toast = new NotificationWindow();

// Create an instance of a UserControl called SampleToast
// that is defined in XAML
var toastContent = new SampleToast();

// Set the content of the notification window to that of the UserControl
toast.Content = toastContent;

// Optionally set size to be different from the
// default of 400 x 100
toast.Width = 200;
toast.Height = 50;

// Show notification for 2 seconds
toast.Show(5000);

MSDN Docs

 

SHARE:

Hosting HTML Content in Silverlight 4

In Silverlight 4 we can present HTML content to the user.

The WebBrowser Control

For example to display the content of DontCodeTired.com we can set the Source property in XAML:

<WebBrowser Name="webBrowser" Source="http://www.dontcodetired.com" />

Or we can use the Navigate method of WebBrowser:

webBrowser.Navigate(new Uri(@"http://www.dontcodetired.com"));

The user can navigate to links, right-click and open in new tab (which opens up tab in a full web browser), view source, etc.

To use the WebBrowser the Silverlight app must be running out of browser (OOB), if you view when embedded in a web page (i.e. not OOB) the control displays a message saying that it can only be used in Out-of-Browser mode.

WebBrowser control running in-browser

WebBrowserBrush

Using the new WebBrowserBrush (previously HtmlBrush in beta 4) we can use the contents of a WebBrowser to fill another element.

For example, to fill an Ellipse with the content of a WebBrowser named "webBrowser" we define a WebBrowserBrush and sets its SourceName:

<Ellipse>
    <Ellipse.Fill>
        <WebBrowserBrush SourceName="webBrowser"  />
    </Ellipse.Fill>
</Ellipse>

The following example creates an ellipse which is filled with a blurred version of the content of a WebBrowser. One thing to note is that we hook into the WebBrowser's LoadCompleted event to ensure the ellipse is updated whenever a page has been loaded into the WebBrowser. When run it looks like the image below: 



XAML:

<UserControl x:Class="SL4HTMLHosting.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"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">

    <Grid x:Name="LayoutRoot" Background="White">
        <Grid.RowDefinitions>
            <RowDefinition Height="1*" />
            <RowDefinition Height="1*" />
        </Grid.RowDefinitions>
       
        <WebBrowser Name="webBrowser"
                    Source="http://www.dontcodetired.com"
                    Grid.Row="0"
                    LoadCompleted="webBrowser_LoadCompleted">
        </WebBrowser>
       
        <Ellipse HorizontalAlignment="Stretch"
                    VerticalAlignment="Stretch"
                    Grid.Row="1">
            <Ellipse.Effect>
                <BlurEffect Radius="5" />
            </Ellipse.Effect>
            <Ellipse.Fill>
                <WebBrowserBrush x:Name="browserBrush"
                                    SourceName="webBrowser"  />
            </Ellipse.Fill>
        </Ellipse>
          
    </Grid>
</UserControl>

Code Behind:

using System.Windows.Controls;

namespace SL4HTMLHosting
{
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();
        }

        private void webBrowser_LoadCompleted(object sender, System.Windows.Navigation.NavigationEventArgs e)
        {
            browserBrush.Redraw();
        }
    }
}

Interacting With WebBrowser Content

The following example uses WebBrowser.NavigateToString which allows a string of HTML to be rendered rather than a page at some Uri. The example creates a simple html button which uses window.external.notify to pass a message to the WebBroswer control via it's ScriptNotify event.

XAML:

<Grid x:Name="LayoutRoot" Background="White">            
    <WebBrowser Name="webBrowser"
                ScriptNotify="webBrowser_ScriptNotify">                  
    </WebBrowser>          
</Grid>

Code Behind:

using System.Windows.Controls;
using System.Text;
using System.Windows;

namespace SL4HTMLHosting
{
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();
           
            // Create some basic HTML on the fly containing a simple button,
            // that when clicked passes a message to the containing WebBrowser
            var html = new StringBuilder();
            html.Append("<html>");
            html.Append("   <body>");
            html.Append(@"       <input type=""button"" value=""Show Message Box"" onclick=""window.external.notify('button click');"" /> ");
            html.Append("   </body>");
            html.Append("</html>");

            webBrowser.NavigateToString(html.ToString());
        }



        private void webBrowser_ScriptNotify(object sender, NotifyEventArgs e)
        {
            MessageBox.Show(string.Format("Message from web page: '{0}'.",e.Value));
        }
    }
}
 

SHARE:

Right Click and Context Menu Support in Silverlight 4

Silverlight 4 allows us access to right-click functionality, in earlier versions of Silverlight right-clicking would bring up the standard SL popup menu.

Simple Right Click

To capture a right-click the MouseRightButtonDown event can be subscribed to:

MouseRightButtonDown="btnPopup_MouseRightButtonDown"

To prevent the standard SL popup from appearing, in the event handler we set Handled to true, this stops the right-click event from bubbling up to Silverlight:

private void btnPopup_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{   
    // Prevent the standard Silverlight popup          
    e.Handled = true;

    MessageBox.Show("Right button clicked");
}

Context Menu

The ContextMenu is available in the Silverlight Toolkit.

A popup context menu can easily be added to a control (e.g. a button), when the control is right-clicked the context menu is automatically shown

The XAML below declares a simple popup menu with 2 menu items: Open & Save that when clicked both fire the same event handler.

<Button Name="btnContextMenu"
        Content="ContextMenu Right Click"
        MouseRightButtonDown="btnContextMenu_MouseRightButtonDown">
    <toolkit:ContextMenuService.ContextMenu>
        <toolkit:ContextMenu>
            <toolkit:MenuItem Header="Open" Click="ContextMenuItem_Click"/>
            <toolkit:MenuItem Header="Save" Click="ContextMenuItem_Click"/>
        </toolkit:ContextMenu>                   
    </toolkit:ContextMenuService.ContextMenu>     
</Button>

The namespace (xmlns:toolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit") must be declared - if you drag and drop something from the Toolkit the namespace will be automatically added.

Full listing (simple popup & context menu)

XAML:

<UserControl x:Class="SL4RClickPopup.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:toolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400" >  
   
    <StackPanel>
       
        <Button Name="btnPopup"
                Content="Simple Right Click"
                MouseRightButtonDown="btnPopup_MouseRightButtonDown" />
       
        <Button Name="btnContextMenu"
                Content="ContextMenu Right Click"
                MouseRightButtonDown="btnContextMenu_MouseRightButtonDown">
           
            <toolkit:ContextMenuService.ContextMenu>
                <toolkit:ContextMenu>
                    <toolkit:MenuItem Header="Open" Click="ContextMenuItem_Click"/>
                    <toolkit:MenuItem Header="Save" Click="ContextMenuItem_Click"/>
                </toolkit:ContextMenu>
            </toolkit:ContextMenuService.ContextMenu>
           
        </Button>
         
    </StackPanel>
   
</UserControl>

Code Behind:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace SL4RClickPopup
{
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();
        }



        private void btnPopup_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
        {
            // Prevent the standard Silverlight popup          
            e.Handled = true;

            MessageBox.Show("Right button clicked");
        }



        private void btnContextMenu_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
        {
            // Because we have declare a ContextMenu in Xaml we don't
            // need to manually set e.Handled = true;
        }



        private void ContextMenuItem_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show("ContextMenu item clicked");
        }
    }
}
 

SHARE:

Drag and Drop in Silverlight 4

To enable drag and drop support on a Silverlight UIElement the AllowDrop property needs to be set to True (it is false by default). The Drop event can now be subscribed to, in which the dropped item(s) can be processed.

The example below allows a .jpg file to be dragged (from the desktop, Windows Explorer, etc.) and dropped onto the application. When the user drops a .jpg, the image is loaded and displayed.

<Grid x:Name="LayoutRoot"
        Background="Black"
        AllowDrop="True"
        Drop="LayoutRoot_Drop">
       
    <Image Name="imgPresenter"
            Stretch="UniformToFill"
            IsHitTestVisible="False" />
       
    <TextBlock HorizontalAlignment="Center"
                VerticalAlignment="Center"
                IsHitTestVisible="False"
                Foreground="#FFCBC3C3"
                FontSize="32"
                Opacity="0.33"
                Text="drag photo here"/>       
       
</Grid>

 

Code behind:

private void LayoutRoot_Drop(object sender, DragEventArgs e)
{
    if (e.Data != null)
    {
        // Get a list of the file(s) that the user has dragged & dropped
        FileInfo[] droppedFiles = e.Data.GetData(DataFormats.FileDrop) as FileInfo[];

        // If there is at least 1 file that has been dropped
        if (droppedFiles != null && droppedFiles.Length > 0)
        {
            LoadImage(droppedFiles.First());
        }               
    }
}



private void LoadImage(FileInfo imageFileInfo)
{
    if (imageFileInfo.Extension.ToUpper() != ".JPG")
    {
        MessageBox.Show("Only .jpg files are supported...");
    }
    else
    {
        // Load the image from disk
        BitmapImage source = new BitmapImage();
        source.SetSource(imageFileInfo.OpenRead());

        // Set the image being displayed to the new dropped one
        imgPresenter.Source = source;
    }
}

SHARE:

Copy and Paste in Silverlight 4

Silverlight 4 added the static Clipboard class with the clearly-named SetText, GetText and ContainsText methods.

If running in-browser or out-of-browser without elevated trust the Clipboard methods can only be called as a result of user actions (such as Click, KeyDown). The first time a method is called, a security prompt is presented to the user asking them to grant the app access to the clipboard.



If the user selects 'No' a SecurityException is thrown.

 

If the app is running out-of-browser with elevated trust, the above dialog is not shown and the Clipboard methods can be called even if not in response to user initiated events such as in your page constructor, in a DispatcherTimer.Tick event, etc.

Example Code

<Button x:Name="btnCopy" Click="btnCopy_Click">Copy</Button>
<Button x:Name="btnPaste" Click="btnPaste_Click">Paste</Button>
<TextBox x:Name="txtBox"></TextBox>

 

private void btnCopy_Click(object sender, RoutedEventArgs e)
{
    // Assumes we want to prevent the user from copy 'nothing' to the clipboard.
    // Without this check, whatever text is currently in the clipboard will be replaced
    // with nothing.
    if (!string.IsNullOrEmpty(txtBox.Text))
    {
        try
        {
            Clipboard.SetText(txtBox.Text);
        }
        catch (SecurityException ex)
        {
            // If the user does not grant access for the app to access the clipboard
            // a SecurityException is thrown
            MessageBox.Show("Clipboard access is required to use this feature.");
        }
    }
}


private void btnPaste_Click(object sender, RoutedEventArgs e)
{
    try
    {
        txtBox.Text = Clipboard.GetText();
    }
    catch (SecurityException ex)
    {
        // If the user does not grant access for the app to access the clipboard
        // a SecurityException is thrown
        MessageBox.Show("Clipboard access is required to use this feature.");
    }
}

SHARE:

FallbackValue, TargetNullValue & StringFormat in Silverlight 4

Silverlight 4 introduces some further databinding functionality including:

FallbackValue

If there is a problem with performing the binding, for example an incorrect path to a property value, the fallback value will be used in it's place. This allows some meaningful display, rather that the default behaviour of showing nothing.

TargetNullValue

Allows a value to be used if the bound property is null.

StringFormat

Allows formatting of the bound value using standard formatting expressions.

Example

The following is the output of the XAML below:

 

 

<Grid x:Name="LayoutRoot" Background="WhiteSmoke" >
    <Grid.ColumnDefinitions>
        <ColumnDefinition></ColumnDefinition>
        <ColumnDefinition></ColumnDefinition>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="auto"></RowDefinition>
        <RowDefinition Height="auto"></RowDefinition>
        <RowDefinition Height="auto"></RowDefinition>
        <RowDefinition Height="auto"></RowDefinition>
        <RowDefinition Height="auto"></RowDefinition>
        <RowDefinition Height="auto"></RowDefinition>
    </Grid.RowDefinitions>

    <TextBlock TextWrapping="Wrap">Invalid binding path with FallbackValue set</TextBlock>
    <TextBlock Text="{Binding Path=badPath, FallbackValue='this is a fallback value'}" Grid.Column="1"></TextBlock>

    <TextBlock TextWrapping="Wrap" Grid.Row="1">Invalid binding path withOUT FallbackValue set</TextBlock>
    <TextBlock Text="{Binding Path=badPath}" Grid.Row="1" Grid.Column="1"></TextBlock>

    <TextBlock TextWrapping="Wrap" Grid.Row="2">Bound property = null with TargetNullValue set</TextBlock>
    <TextBlock Text="{Binding Path=AnIntProperty, TargetNullValue='The int is null'}" Grid.Column="1" Grid.Row="2"></TextBlock>

    <TextBlock TextWrapping="Wrap" Grid.Row="3">Bound property = null withOUT TargetNullValue set</TextBlock>
    <TextBlock Text="{Binding Path=AnIntProperty}" Grid.Column="1" Grid.Row="3"></TextBlock>

    <TextBlock TextWrapping="Wrap" Grid.Row="4">Bound property with StringFormat</TextBlock>
    <TextBlock Text="{Binding Path=AnotherIntProperty, StringFormat='Age is {0} years old.'}" Grid.Column="1" Grid.Row="4"></TextBlock>

    <TextBlock TextWrapping="Wrap" Grid.Row="5">Bound property withOUT StringFormat</TextBlock>
    <TextBlock Text="{Binding Path=AnotherIntProperty}" Grid.Column="1" Grid.Row="5"></TextBlock>

</Grid>

The DataContext is set to an instance of the following class:

public class TestData : DependencyObject
{
    public int? AnIntProperty
    {
        get { return (int?)GetValue(AnIntPropertyProperty); }
        set { SetValue(AnIntPropertyProperty, value); }
    }

    // Using a DependencyProperty as the backing store for AnIntProperty.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty AnIntPropertyProperty =
        DependencyProperty.Register("AnIntProperty", typeof(int?), typeof(TestData),
            new PropertyMetadata(0));


    public int AnotherIntProperty
    {
        get { return (int)GetValue(AnotherIntPropertyProperty); }
        set { SetValue(AnotherIntPropertyProperty, value); }
    }

    // Using a DependencyProperty as the backing store for AnotherIntProperty.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty AnotherIntPropertyProperty =
        DependencyProperty.Register("AnotherIntProperty", typeof(int), typeof(TestData), new PropertyMetadata(0));


    public TestData()
    {
        AnIntProperty = null;
        AnotherIntProperty = 44;
    }

}

SHARE:

Printing Support in Silverlight 4

Basic printing can be accomplished through the PrintDocument class. For example, to print the entire contents of the LayoutRoot grid:

private void btnSimplePrint_Click(object sender, RoutedEventArgs e)
{
    string docName = "Test SL4 Print";

    PrintDocument doc = new PrintDocument();

    doc.PrintPage += (s, args) =>
    {
        // print the entire contents of our page (i.e. the
        // outer grid called LayoutRoot)
        args.PageVisual = LayoutRoot;
    };

    doc.Print(docName); // automatically shows a print dialog box
}

Various properties are avaible on the PrintPageEventArgs that are passed to the PrintPage event handle ('args' in the above example); args.HasMorePages allows you to control multi-page printing and args.PrintableArea allows you to get the printable page area you have available to you (which will depend on the settings chosen in the print dialog box that is shown automatically for you.

It is up to the developer to handle more complex scenarios such as overflowing contents, knowing if there are more pages required, etc. In an enterprise situation other PDF-scentric options may be a better choice, especially if there are complex report layouts required, etc. For example, using iText.NET, FOP, or an enterprise solution such as Thunderhead, xPression, etc. 

SHARE: