Automated Attendant with the OCSDK Wrapper and WPF
Our office recently cut over to using Office Communications Server for our telephony needs, thereby replacing our old Nortel PBX. One of the side effects of this was that we no longer have a phone in our lobby for visitors to use to call the receptionist. Instead of purchasing a Polycom CX700 IP Phone and figuring a way to make sure it doesn't get stolen in our lobby, I wrote the Clarity Attendant app that allows visitors to search for consultants and place a call.
WPF Keyboard Control
By far the hardest part was creating the WPF keyboard for users to enter partial names of the consultants they'd like to find. This involves creating a keyboard button user control and dropping these onto a Keyboard control that would listen to click events and raise them up to it consumer as needed.
Keyboard Button Control
The Keyboard Button control is a container for all buttons on the keyboard.
XAML
I am by no means a designer. In order to allow a designer to modify the buttons as needed, I've placed the button within a user control that a designer can then take into Expression Blend and design to their hearts content. The code below is pretty simple.
Code
The keyboard button code-behind is pretty simple. It exposes a properties for Text and ButtonType which can be set by the keyboard control. The ButtonType helps us handle special keys such as space, backspace, and clear.When the button is clicked, I raise it as an event through the ButtonEventManager, described later.
using System.Windows;
using System.Windows.Controls;
namespace UCKiosk.UserControls
{
/// <summary>
/// Interaction logic for KeyboardButton.xaml
/// </summary>
public partial class KeyboardButton : UserControl
{
public string Text { get; set; }
public ButtonType ButtonTypeEnum { get; set; }
public static double WIDTH = 95;
public static double HEIGHT = 60;
public KeyboardButton()
{
InitializeComponent();
Loaded += KeyboardButton_Loaded;
MyButton.Click += MyButton_Click;
}
void KeyboardButton_Loaded(object sender, RoutedEventArgs e)
{
SetupDimensions();
MyButton.Content = Text;
}
private void SetupDimensions()
{
if (double.IsNaN(Width))
this.Width = WIDTH;
if (double.IsNaN(Height))
this.Height = HEIGHT;
MyButton.Width = this.Width;
MyButton.Height = this.Height;
this.HorizontalAlignment = HorizontalAlignment.Left;
this.VerticalAlignment = VerticalAlignment.Top;
MyButton.FontSize = 18;
}
void MyButton_Click(object sender, RoutedEventArgs e)
{
ButtonClickedEventArgs evt = new ButtonClickedEventArgs(Text,ButtonTypeEnum);
ButtonEventManager.Instance.OnButtonClickedEvent(evt);
}
public enum ButtonType
{
Backspace=1,
Space=2,
Clear=3
}
}
}
WPF Keyboard Control
XAML
Now that we've crated our keyboard buttons control, we need to drop them onto a surface that will arrange these buttons for us. I did this by creating 3 separate StackPanels and arranging the keyboard buttons horizontally in them. I added x:Name properties for the special controls since I'll be altering their sizes programtically in the code behind. I've also tagged what type of buttons they are so that I can handle the special buttons in code.
<UserControl xmlns:my="clr-namespace:System.Windows.Controls.Primitives;assembly=PresentationFramework" x:Class="UCKiosk.UserControls.WPFKeyboard"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:UC="clr-namespace:UCKiosk.UserControls" >
<Canvas>
<StackPanel x:Name="Row1" Orientation="Horizontal">
<UC:KeyboardButton Text="Q" />
<UC:KeyboardButton Text="W" />
<UC:KeyboardButton Text="E" />
<UC:KeyboardButton Text="R" />
<UC:KeyboardButton Text="T" />
<UC:KeyboardButton Text="Y" />
<UC:KeyboardButton Text="U" />
<UC:KeyboardButton Text="I" />
<UC:KeyboardButton Text="O" />
<UC:KeyboardButton Text="P" />
</StackPanel>
<StackPanel x:Name="Row2" Orientation="Horizontal" >
<UC:KeyboardButton Text="A" />
<UC:KeyboardButton Text="S" />
<UC:KeyboardButton Text="D"/>
<UC:KeyboardButton Text="F" />
<UC:KeyboardButton Text="G" />
<UC:KeyboardButton Text="H" />
<UC:KeyboardButton Text="J" />
<UC:KeyboardButton Text="K" />
<UC:KeyboardButton Text="L" />
<UC:KeyboardButton x:Name="Backspace" Text="Backspace" ButtonTypeEnum="Backspace" />
</StackPanel>
<StackPanel x:Name="Row3" Orientation="Horizontal" >
<UC:KeyboardButton Text="Z"/>
<UC:KeyboardButton Text="X" />
<UC:KeyboardButton Text="C" />
<UC:KeyboardButton Text="V" />
<UC:KeyboardButton Text="B" />
<UC:KeyboardButton Text="N" />
<UC:KeyboardButton Text="M" />
<UC:KeyboardButton x:Name="Clear" Text="Clear" ButtonTypeEnum="Clear" />
</StackPanel>
<StackPanel x:Name="Row4" Orientation="Horizontal" >
<UC:KeyboardButton x:Name="Space" Text="Space" ButtonTypeEnum="Space"/>
</StackPanel>
</Canvas>
</UserControl>
Code
In the code-behind, I handle the events of the individual keyboard buttons, as described in the ButtonEventManager below, and set up some sizing.
using System;
using System.Windows;
using System.Windows.Controls;
namespace UCKiosk.UserControls
{
/// <summary>
/// Interaction logic for WPFKeyboard.xaml
/// </summary>
///
public partial class WPFKeyboard : UserControl
{
public delegate void KeyPressedDelegate(object sender, ButtonClickedEventArgs e);
public event KeyPressedDelegate KeyPressed;
public WPFKeyboard()
{
InitializeComponent();
InitializeButtonSizing();
ButtonEventManager.Instance.ButtonClickedEvent += Instance_ButtonClickedEvent;
Unloaded += WPFKeyboard_Unloaded;
}
private void InitializeButtonSizing()
{
var width = KeyboardButton.WIDTH;
var height = KeyboardButton.HEIGHT;
Clear.Width = width * 2;
Space.Width = width * 9;
Backspace.Height = height * 3;
Row2.Margin = new Thickness(0, height, 0, 0); // Margin="15,30,0,0"
Row3.Margin = new Thickness(0, height * 2, 0, 0); //Margin="30,60,0,0"
Row4.Margin = new Thickness(0, height * 3, 0, 0); //Margin="0,90,0,0"
}
void Instance_ButtonClickedEvent(object sender, EventArgs e)
{
var args = (ButtonClickedEventArgs)e;
if (KeyPressed != null)
KeyPressed(sender, args);
}
void WPFKeyboard_Unloaded(object sender, RoutedEventArgs e)
{
ButtonEventManager.Instance.ButtonClickedEvent -= Instance_ButtonClickedEvent;
}
}
}
ButtonEventManager
The goal of the larger Keyboard control is to raise an event to its consumer that a key was clicked. Without this class, each keyboard button would have its own button clicked event that the consumer of the keyboard control would have to wire up. I handled this by creating a class called ButtonEventManager. This class is responsible for maintaining a collection of all individual button clicked events and raising them to the consumer of the keyboard control. This is done by using a EventHandlerList, which is a collection of delegates. Note that I've implemented this class as a singleton so that there only exists one instance of this object per AppDomain.
using System;
using System.ComponentModel;
namespace UCKiosk.UserControls
{
public sealed class ButtonEventManager
{
private ButtonEventManager() { }
public static readonly ButtonEventManager Instance = new ButtonEventManager();
private EventHandlerList EventList = new EventHandlerList();
private static object ButtonClickedEventKey = new object();
public event EventHandler ButtonClickedEvent
{
add
{
EventList.RemoveHandler(ButtonClickedEventKey, value);
EventList.AddHandler(ButtonClickedEventKey, value);
}
remove
{
EventList.RemoveHandler(ButtonClickedEventKey, value);
}
}
public void OnButtonClickedEvent(ButtonClickedEventArgs e)
{
if (EventList[ButtonClickedEventKey] != null)
{
((EventHandler)EventList[ButtonClickedEventKey]).Invoke(this, e);
}
}
}
public class ButtonClickedEventArgs : EventArgs
{
public string Text { get; set; }
public KeyboardButton.ButtonType Type { get; set; }
public ButtonClickedEventArgs(string text, KeyboardButton.ButtonType type )
{
Text = text;
Type = type;
}
}
}
Attendant Application
The Attendant application contains our keyboard control we just made along with a listview to query results, a logo and a call button.
XAML
<Window xmlns:my="clr-namespace:System.Windows.Controls;assembly=PresentationFramework" x:Class="UCKiosk.Home"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:UC="clr-namespace:UCKiosk.UserControls"
Title="Home" Width="1024" Height="768" Foreground="#FF74AFE8" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" Opacity="1">
<Canvas >
<Rectangle Stroke="#FF3184C3" Width="1024" Height="768">
<Rectangle.Fill>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FF74AFE8" Offset="0.379"/>
<GradientStop Color="#FF3184C3" Offset="0.746"/>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Button FontSize="30" x:Name="btnCall" Click="btnCall_Click" Height="58" Canvas.Left="888" Canvas.Top="18" Width="87" Content="Call"/>
<UC:WPFKeyboard HorizontalAlignment="Center" x:Name="objKeyboard" Margin="30,425,0,0"/>
<TextBox TextChanged="txtInput_TextChanged" FontSize="18" x:Name="txtInput" Width="848.5" Height="39.5" Canvas.Left="34" Canvas.Top="380" />
<ListView FontSize="14" x:Name="lvwResults" Height="356" Width="266.5" Canvas.Left="616" Canvas.Top="18" >
<ListView.GroupStyle>
<GroupStyle AlternationCount="2">
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Style.Triggers>
<Trigger Property="ItemsControl.AlternationIndex" Value="1">
<Setter Property="Background" Value="LightBlue"></Setter>
</Trigger>
</Style.Triggers>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
<Image Width="379.6" Height="367.144" Canvas.Left="98" Source="ClarityAttendant.png" Stretch="Fill" Canvas.Top="6.856"/>
</Canvas>
</Window>
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Querying Active Directory
In order to search for users, I am using LDAP queries against Active Directory to retrieve all users that have Communicator enabled. I do this upon startup of the application and then store the results in a list that I can so that I'm not taxing our domain controllers too heavily. I store the user's name in a List<string> collection since I can search for users easily. I store the SIP URI's for Office communicator in a hash table. When a user clicks call, I look up the users's SIP URI via their displayName and place the call.
private void PopulateUsers()
{
try
{
var entry = new DirectoryEntry {Path = "LDAP://CN=Users,DC=YOURDOMAIN,DC=COM"};
var searcher = new DirectorySearcher
{
SearchRoot = entry,
Filter = "(&(objectClass=user)(cn=*)(displayName=*)(msRTCSIP-UserEnabled=*))"
};
searcher.PropertiesToLoad.Add("displayName");
searcher.PropertiesToLoad.Add("msRTCSIP-PrimaryUserAddress");
var results = searcher.FindAll();
if (results.Count > 0)
{
foreach (SearchResult result in results)
{
var adUser = (result.Properties["displayName"][0].ToString());
var sipURI = result.Properties["msRTCSIP-PrimaryUserAddress"][0].ToString();
_sipURIs.Add(adUser, sipURI);
_users.Add(adUser);
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
MessageBox.Show(ex.Message);
}
}
Searching for Users
Searching for users is fairly simple. We have to create our own delegate method to perform the searching and call it from the List's Find method
private void SearchForName()
{
var results = _users.FindAll(ContainsUserText);
lvwResults.ItemsSource = results;
if (results.Count == 1)
lvwResults.SelectedIndex = 0;
}
private bool ContainsUserText(string s)
{
return s.ToUpper().IndexOf(txtInput.Text) >= 0;
}
Placing the call
Placing the call is probably the easiest part of this application, thanks to the OCSDKWrapper Project on CodePlex. I simply look up the selected user's SIPURI and call the CallComputer() Method
private void btnCall_Click(object sender, RoutedEventArgs e)
{
if (txtInput.Text == string.Empty)
return;
var sipUri = _sipURIs[(string) lvwResults.SelectedItem];
MOCAutomation.Instance.CallComputer(sipUri);
ResetAppState();
}