RGS Agent OC Tab with Silverlight

I’ve already described how you would create your own client using the RGS Agent WebService. Now what if you want to keep the tab in OC but make it look more fancy? Silverlight of course is your friend here. This post describes how you can create your own Silverlight based Agent tab, which will look like following. Btw, I am using Visual Studio 2008 to build this entire solution.

AgentTab

All the code for this can be found in AgentTabSl.zip (45.3 KB).

Getting Started

First I created a new Silverlight Application project and named it ‘AgentTabSl’. The project template already comes with the basic structure which we need for this Silverlight control. I added another Silverlight user control which I use to show one Agent Group, named ‘Group.xaml’. Its XAML looks like following.

<UserControl x:Class="AgentTabSl.Group"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid x:Name="LayoutRoot" Background="White">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>

        <Rectangle Width="Auto" Height="Auto" Stroke="Black" RadiusX="4" RadiusY="4"
                   Grid.ColumnSpan="3" Grid.RowSpan="2" Name="Back" />

        <CheckBox Grid.RowSpan="2" Content="" VerticalAlignment="Center" Name="SignedIn"
                  Margin="2" Cursor="Hand" Checked="OnSignedInChanged" Unchecked="OnSignedInChanged" />

        <TextBlock Grid.Column="1" Grid.ColumnSpan="2" Name="GroupName" Margin="0,2"
                   FontWeight="Bold" />

        <TextBlock Grid.Column="1" Grid.Row="1" Margin="0,2">Number of Agents</TextBlock>

        <TextBlock Grid.Column="2" Grid.Row="1" TextAlignment="Right" Name="AgentCount"
                   Margin="2,2,4,2" />
    </Grid>
</UserControl>

I also added some code to the Group class to allow setting the agent group name, whether or not it’s a formal agent group, if the user is currently signed in and the number of agents. On top of this, I added a static method which return a new instance of Group created based on the required parameters.

Retrieving the data from the Web Service

Unfortunately, because of the limitations of the Silverlight runtime, we cannot use the client proxy generated from wsdl.exe here. But you can still add a reference to the web service, e.g. through the ‘Add Service Reference …’ command in Visual Studio. Simply enter the url to a deployed RGS Agent WebService (e.g. https://ocs-pool-01.contoso.com/Rgs/Clients/ProxyService.asmx) and everything else will be done for you. It is worthwhile to note that all methods of the web service will be asynchronous, but that’s not a problem. I’ve added an event handler for the Loaded event of the Silverlight application:

private void OnLoaded(object sender, RoutedEventArgs e)
{
    _service = new ProxyServiceSoapClient();

    _service.Endpoint.Address = new EndpointAddress(
        "https://ocs-pool-01.contoso.com/Rgs/Clients/ProxyService.asmx");

    _service.GetGroupsCompleted += OnGetGroupsCompleted;
    _service.GetAgentCompleted += OnGetAgentCompleted;
    _service.SignInCompleted += OnSignInCompleted;
    _service.SignOutCompleted += OnSignOutCompleted;

    _service.GetAgentAsync();

    _timer = new Timer(OnTimer, null, 0, 60 * 1000);
}

Timer is from System.Threading and I use it to periodically (every 60 seconds) refresh the data from the web service. This is required because else the Silverlight application won’t figure out if groups have been added / removed / modified. You should not make it refresh in less than 60 seconds, but in the end, it is your decision (and your servers).

Please keep in mind that if you want to deploy this Silverlight application on a web server different than the OCS WebComponents, you’ll need to allow Silverlight to access to that server. Please read the HTTP Communication and Security with Silverlight topic on MSDN for more information.

Retrieving the agent group memberships

The following code snippet shows the event handler for the GetGroupsCompleted event generated for us through the service reference. The first thing I am doing is to check whether or not we’re on the UI thread. If we’re not, we have to use the Dispatcher to invoke the method on the UI thread. The actual code to add the groups is then as simple as going through all groups and creating a new instance of the Group control which we’ll add to the Children collection of the StackPanel which holds the groups.

private void OnGetGroupsCompleted(object sender, GetGroupsCompletedEventArgs e)
{
    if (!Dispatcher.CheckAccess())
    {
        Dispatcher.BeginInvoke(() => OnGetGroupsCompleted(sender, e));
    }
    else
    {
        IEnumerable<AcdGroup> groups = e.Result.OrderBy(group => group.Name);

        Groups.Children.Clear();

        foreach (AcdGroup group in groups)
        {
            Group ctrl = Group.Create(group.Id, group.CanSignIn, group.IsSignedIn,
                                      group.Name, group.NumberOfAgents);

            ctrl.SignedInChanged += OnSignedInChanged;

            Groups.Children.Add(ctrl);
        }

        SetStatus("Ready.");
    }
}

Sign in / Sign out

Now what’s left is basically to let the user sign in / out throug a click on the checkbox for the corresponding group. As you can see in the code (when you downloaded it), I also added an event to the Group control which is fired when the sign-in state for the group is changed. In the code above you see that we’re subscribing to that event on line 18. So all we need to do is to actually call the corresponding method on the web service when the event is fired.

private void OnSignedInChanged(object sender, EventArgs e)
{
    Group group = sender as Group;

    SetStatus("Please wait while the operation is performed ...");

    if (group.IsSignedIn)
    {
        _service.SignInAsync(group.Id);
    }
    else
    {
        _service.SignOutAsync(group.Id);
    }
}

What’s missing?

If you look at the interface of the web service again, you see that it also allows you to sign in / out with multiple groups at the same time. I’ll leave it to you to implement that in this Silverlight application as well as it does not really involve anything which I haven’t discussed here. Also, you may want to highlight groups which have just been added by using a different gradient for the box.

If you’re planning on actually deploying a Silverlight Agent OC Tab, you most likely also want to make sure that the users get the static strings in their preferred language. The article Deployment and Localization on MSDN should give you all the information required to do that.

Fun with JSON and WCF

One of the projects I am working on at home is something like a media player web application which I use to listen to my favorite music from anywhere. It has a backend database which keeps the music files in a way which allows fast search on information about the files from the tags in the files. The files are then played in a handcrafted media player written in Silverlight 2.0. This being a fancy web 2.0 application, I use AJAX to search the database and return the results as JSON objects. Luckily enough, WCF supports you with this since .net 3.5. All you basically need to do is create a WCF service for your web project in Visual Studio 2008 (SP1). Then you even have IntellliSense support for the client side wrapper of the service, which of course gets generated automatically. On top of this, you do not have to care too much about inter-browser compatibility: The generated scripts with the base libraries work fine with both IE and Firefox.

If on the other hand, you are running the application on a virtual site in IIS which supports multiple host headers (let’s say: foo.bar.com and www.foo.bar.com) you’re likely to run into an exception like the following.

[ArgumentException: This collection already contains an address with scheme http. There can be at most one address per scheme in this collection.
Parameter name: item]
   System.ServiceModel.UriSchemeKeyedCollection.InsertItem(Int32 index, Uri item) +11520590
   System.Collections.Generic.SynchronizedCollection`1.Add(T item) +67
   System.ServiceModel.UriSchemeKeyedCollection..ctor(Uri[] addresses) +49
   System.ServiceModel.ServiceHost..ctor(Type serviceType, Uri[] baseAddresses) +129
   System.ServiceModel.Activation.ServiceHostFactory.CreateServiceHost(Type serviceType, Uri[] baseAddresses) +28
   System.ServiceModel.Activation.ServiceHostFactory.CreateServiceHost(String constructorString, Uri[] baseAddresses) +331
   System.ServiceModel.HostingManager.CreateService(String normalizedVirtualPath) +11659932
   System.ServiceModel.HostingManager.ActivateService(String normalizedVirtualPath) +42
   System.ServiceModel.HostingManager.EnsureServiceAvailable(String normalizedVirtualPath) +479

This indicates that a binding address which starts with http is already in use, when trying to automatically add the second address. The solution to this is as simple as updating your Web.Config file like shown here. Please take a look at line #9:

<configuration>
    <!-- ... -->
    <system.serviceModel>
        <behaviors>
            <!-- ... -->
        </behaviors>
        <serviceHostingEnvironment aspNetCompatibilityEnabled="true">
            <baseAddressPrefixFilters>
                <add prefix="http://foo.bar.com" />
            </baseAddressPrefixFilters>
        </serviceHostingEnvironment>
        <services>
            <!-- ... -->
        </services>
        <bindings>
            <!-- ... -->
        </bindings>
    </system.serviceModel>
</configuration>

Addendum from Feb 6: Apparently I was not entirely correct about using the service on a site with multiple host headers. While the above changes to Web.config fix the initial problem, they introduce a new problem. You’ll realize that this will work only for the requests using one of the host headers. The request to the service using the other host headers will throw a 404. This is a reported bug and will hopefully be fixed soon.