May 2009 - Posts
The source code for this walkthrough can be downloaded here.
I compiled this walkthrough for MOC 6461 that I am teaching these days. It corresponds to the work convered by units 1 and 2 of the MOC and will guide you through the following steps:
- Define the Contracts
- Implement the Contracts
- Implement a WCF Service hosted in IIS
- Create a Client Application
- Add a Self-Hosted Console Application for the Service
- Modify the Client Application to use the Console Host
- Add a Self-Hosted Windows Service for the Service
- Test the Client Application with the Windows Service Host
Define the Contracts
- Create a new project of type class library called “InventoryContracts”.
- Rename the Class1.cs file to IInventory.cs and add the following types:
namespace InventoryContracts
{
public class Product
{
public string Name { get; set; }
public int UnitsInStock { get; set; }
}
public interface IInventory
{
Product[] GetProducts();
bool AddProduct(Product newProduct);
}
}
- Add references to the following assemblies: System.ServiceModel and System.Runtime.Serialization.
- Decorate the types with contract attributes as follows:
namespace InventoryContracts
{
[DataContract]
public class Product
{
[DataMember]
public string Name { get; set; }
[DataMember]
public int UnitsInStock { get; set; }
}
[ServiceContract]
public interface IInventory
{
[OperationContract]
Product[] GetProducts();
[OperationContract]
bool AddProduct(Product newProduct);
}
}
Implement the Contracts
- Create a new project of type class library called “InventoryAccessLibrary”.
- Rename Class1.cs to InventoryAccess.cs (and accept the name change for the class).
- Add a new Data Source to the project (Data -> Add New Data Source).
- In the new data source wizard select “Database” and add a connection to the Northwind database.
- Call the data set NothwindDataSet and add one DataTable corresponding to Products.
- Add a reference to the InventoryContracts assembly.
- In InventoryAccess.cs add using directives for the InventoryContracts and InventoryAccess.NorthwindDataSetTableAdapters namespaces.
- Have Inventory implement the IInventory interface as follows:
namespace InventoryAccessLibrary
{
public class InventoryAccess : IInventory
{
#region IInventory Members
public Product[] GetProducts()
{
ProductsTableAdapter tableAdapter = new ProductsTableAdapter();
var query = from NorthwindDataSet.ProductsRow row in tableAdapter.GetData()
select new Product {
Name = row.ProductName,
UnitsInStock = row.UnitsInStock };
return query.ToArray();
}
public bool AddProduct(Product newProduct)
{
ProductsTableAdapter tableAdapter = new ProductsTableAdapter();
// Use default values and the valid SupplierId of 1
int rowsChanged = tableAdapter.Insert(
newProduct.Name, null, null, "1", null, null, null, null, false);
return rowsChanged == 1;
}
#endregion
}
}
Implement a WCF Service hosted in IIS
- Create a new web site of type WCF Service named InventoryServiceIIS.
- Add a reference to the InventoryAccessLibrary project.
- Delete the files Service.cs and IService.cs in the App_Code folder.
- Change the single line in the Services.svc file to read:
<%@ ServiceHost Language="C#" Debug="true" Service="InventoryAccessLibrary.InventoryAccess" %>
- In the web.config file, make the following changes to the system.serviceModel element (at the end of the file).
- The service name should be “InventoryAccessLibrary.InventoryAccess” instead of “Service”.
- The contract attribute of the end point should be “InventoryContracts.IInventory” instead of IService.
- Also change the binding from “wsHttpBinding” to “basicBinding”.
- Enable debugging: Set the value of the debug attribute in the <compilation> element to “true”.
- Use port 11111:
- In Solution Explorer right click on the InventoryServiceIIS node.
- Go to the Properties window and change “Use dynamic ports” to false.
- Change the port field to 11111.
- Test the service:
- In Solution Explorer right click on Service.svc and select View in Browser.
- Verify that your browser successfully opens the test harness for the Inventory service.
Create a Client Application
- Create a new web site of type Console Application named InventoryClientApp.
- Add a reference to the InventoryContracts library.
- Add a service reference to the InventoryServiceIIS service and specify ServiceReferences as the namespace for the proxy class.
- In Program.cs add using directives for the InventoryContracts and InventoryClientApp.ServiceReferences namespaces.
- Consume the service by adding the following code in the Program.cs file:
namespace InventoryClientApp
{
class Program
{
static void Main(string[] args)
{
InventoryClient client = new InventoryClient();
foreach (Product product in client.GetProducts())
{
Console.WriteLine("{0} {1}", product.Name, product.UnitsInStock);
}
Console.ReadLine();
}
}
}
- Run the client and verify that it can retrieve the product information from the service.
Add a Self-Hosted Console Application for the Service
- Create a new project of type Console Application named InventoryServiceConsole.
- Add references to System.ServiceModel and to InventoryAccessLibrary.
- In Program.cs add using directives for the System.ServiceModel, InventoryContracts and InventoryAccessLibrary namespaces.
- Implement hosting of the InventoryAccessLibrary.InventoryAccess class by adding the following code in the Program.cs file:
namespace InventoryServiceConsole
{
class Program
{
static void Main(string[] args)
{
Type serviceType = typeof(InventoryAccess);
Uri baseAddress = new Uri(
"http://localhost:11112/InventoryServiceConsole/");
using (ServiceHost host = new ServiceHost(serviceType, baseAddress))
{
string endPointAddress = "Service.svc";
host.AddServiceEndpoint(
typeof(IInventory),
new BasicHttpBinding(),
endPointAddress);
host.Open();
Console.WriteLine("Listening for requests on {0}",
baseAddress + endPointAddress);
Console.ReadLine();
}
}
}
}
Modify the Client Application to use the Console Host
- Open the app.config file of the Client Applicationץ In the endpoint element, change the address to: http://localhost:11112/InventoryServiceConsole/Service.svc
- Test the console hosted service:
- Right click the InventoryServiceConsole project and select “Set as Start Up Project”.
- Control+F5 to start the host outside the debugger.
- Make sure the WCF service hosted in IIS is closed (e.g. from the icon on the system tray).
- Right click the InventoryClientApp project and select Debug->Start new instance.
- Verify that though the WCF Web Service Site is closed, the client functions correctly.
Add a Self-Hosted Windows Service for the Service
- Create a new project of type Windows Service named InventoryWindowsService.
- Add references to System.ServiceModel and to InventoryAccessLibrary. Rename the Service1.cs file and class to Service.
- In Service.cs copy the code from the self-hosted console application into the OnStart method with the following changes:
- The host variable should be a member not a local variable.
- The host variable should not be disposed of in the OnStart method, instead, Close it in the OnStop method.
- Create installer classes for the Windows service and its process:
- Add a new item of type Installer Class. Name it InventoryServiceInstaller.
- Update the constructor so you have this code in InventoryServiceInstaller.cs:
namespace InventoryWindowsService
{
[RunInstaller(true)]
public partial class InventoryServiceInstaller : Installer
{
public InventoryServiceInstaller()
{
InitializeComponent();
ServiceProcessInstaller pi = new ServiceProcessInstaller();
pi.Account = ServiceAccount.LocalSystem;
ServiceInstaller si = new ServiceInstaller();
si.ServiceName = "InventoryService";
Installers.Add(pi);
Installers.Add(si);
}
}
}
- Install the service:
From the Visual Studio 2008 command prompt navigate to the target folder of the InventoryWindowService project and run the following command:
installutil /i InventoryWindowsService.exe
- Start the service:
From the command prompt run: net start InventoryService.
Test the Client Application with the Windows Service Host
- Verify that the InventoryServiceConsole application is not running.
- Run the InventoryClientApp project
- Verify that though the WCF Web Service Site is not running and the InventoryServiceConsole application is not running, the client functions correctly.
- To clean up, run installutil /u InventoryWindowsService.exe from the command prompt to stop and uninstall the Windows service.
Fiddler 2 is a great tool to monitor the http activity on a computer, and an excellent debugging and teaching tool for WCF and ASMX.
It’s free and you can download the latest version from here.
What I like about it most, is that it can display SOAP messages - request and response as a DOM. You can really see what those contract attributes are doing : )
For my very simple Calculator.Add operation it displayed the following:
Nice, no?
The problem is, that it works best when monitoring traffic between your computer and another IP address. When the server and client are both on your laptop – as they are in my demos – you need to tweak it a bit to get it to work.
There are a number of tweaks on the net, but not all of them work in every case.
Here is a quick summary of the ones I tried. For all tweaks, leave the server as is and modify the address that the client uses in it’s end point.
- Replace “localhost” with “localhost.” (yes, add a dot).
This one worked on my XP machine, but Vista throws an exception. - Replace “localhost” with “127.0.0.1.”
Vista throws an exception.
These two worked for me with Vista Business, IIS 7.0 and with a WCF client and server (Visual Studio 2008).
- Replace “localhost” with “<name of your machine>”.
- Replace “localhost” with “ipv4.fiddler”.
For a WCF client use the following code (change “ipv4.fiddler” to test the other options).
CalculatorServiceClient proxy = new CalculatorServiceClient();
string uriAsString = proxy.Endpoint.Address.Uri.ToString();
string newUriString = uriAsString.Replace("localhost", "ipv4.fiddler");
proxy.Endpoint.Address = new EndpointAddress(newUriString);
result = proxy.Add(20, 11);
By the way, this works for ASMX Web Service clients too. Like so:
Service service = new Service();
service.Url = service.Url.Replace("localhost", "localhost.");
This worked for me on XP - I didnt try it on Vista. I assume that here also, the dot would throw an exception but that I could use the machine name successfully.
When using WSE with ASMX, you need to make an additional change - due to the SOAP headers associated with WS Addressing:
ServiceWse service = new ServiceWse();
string address = service.Url;
string addressVia = service.Url.Replace("localhost", "localhost.");
service.Destination = new EndpointReference
(
new Uri(address),
new Uri(addressVia)
);
Its a bit of a twiddle – but it’s worth checking which option works for you.
It’ll really open up the world of SOAP!
My Visual Studio Side by Side Problem
On my new laptop I had installed VS 2008 but had not done any Web Service development - so I hadn’t yet installed IIS or registered ASP.NET.
Then I installed VS 2010 Beta 1.
A while later I began preparing samples for a course in WCF that I am teaching these days – using Visual 2008. So I installed IIS 7.0 (I am running Vista Business) and registered ASP.Net from the Visual Studio 10.0 command prompt.
I created a simple “Hello World” ASMX Web Service and a “GetData” WCF Service with VS 2008 and hosted them on Local IIS.
Neither worked.
Analysis
Of course, you should be able to run code developed with .Net 3.5 (or even .Net 1.1 for that matter) under ASP.NET 4.0 and you should also be able to host it under ASP.NET 3.5 (2.0 for all intents and purposes) if you so choose.
So why didn’t one of those happen for me?
Well, I had a number of problems – maybe not all related, but I think the essential one was that because I had run aspnet_regiis from the Visual Studio 10.0 command prompt I had installed ASP.NET 4.0 and not 2.0.
So what to do?
Solution
Well, I found I that both options are possible:
Option 1: Run the .Net 3.5 Site under ASP.NET 4.0
There are a number of things you would need to configure in the environment for this to work:
- You need to add the targetFrameworkMoniker=".NETFramework,Version=v4.0” attribute in the <compilation> element of your Web.Config (see the MSDN documentation here).
Visual Studio 2008 doesn’t know about ASP.NET 4.0, so this needs to be done manually. - You need to remove the elements in the <system.codedom> section that explicitly specify use of the compiler for .Net 3.5. Visual Studio 2008 adds these by default to its ASP.NET (ASMX) and WCF Service Web Sites.
This should work for both ASMX and WCF sites with default settings developed with Visual Studio 2008.
Option 2: Just Let Me Target ASP.NET 2.0, Thank You
When I want to run under ASP.NET 4.0 I will use Visual Studio 2010 and get the goodies that go with it. If I am developing an application with Visual Studio 2008 – why can’t I just target ASP 2.0 like I used to?
Well, of course that’s possible, but if you haven’t registered ASP.NET 2.0 with IIS like me, you need to do the following:
- From the Visual Studio 10.0 prompt run: aspnet_regiis.exe –ua to unregister all ASP.NET versions (to be safe).
- From the Visual Studio 2008 prompt run: aspnet_regiis.exe –i to register ASP.NET 2.0.
- Now you may think that thats enough – but its not. You still wont have a mapping for your “.svc” extensions to the aspnet_isapi.dll. This would normally be installed by Visual Studio 2008, but if, like me you installed IIS after installing VS 2008 it wouldnt have been able to do so. So, you need to:
- Go to Control Panel –> Adminstrative Tools –> IIS.
- Select the root node and double click on Handler Mappings in the right pane.
- Click “Add Script Map” and add a new entry with name, say, “svc-3.5”, path “*.svc”. Have the Executable point to the aspnet_isapi.dll in your .Net 2.0 directory (%WINDIR%\Microsoft.NET\Framework\v2.0.XXX).
- Oh, and there is one more thing. As I am running IIS 7.0 I needed to add an Application Pool that would run under ASP.NET 2.0. This is how:
- Go to Control Panel –> Adminstrative Tools –> IIS.
- Select the first child of the root node (Application Pools) and right click to “Add an Application Pool”.
- Configure the new App pool with: Name “ASP.NET v2.0 Classic”, .Net Framework version “.Net Framework v2.0.XX” and managed pipeline mode “Classic”. Check the “start application pool immediately” and OK.
- Select the new Application Pool and make sure its started.
Afterword
Hmmm. It looks like the second option (targeting ASP.Net 2.0/3.5) was much more of a bother than just surrendering to ASP.NET 4.0.
But in my opinion it is the preferred way. Let’s not forget, that when targeting ASP.NET 4.0 you need to make the modifications I described for every new Web Service you create in VS 2008.
Conversely, if you choose to target ASP.NET 2.0 then, in both Visual Studio 2008 and in Visual Studio 2010 Beta 1 you will be able to create ASMX and WCF services with default settings for ASP.NET 2.0/3.5 and ASP.NET 4.0 respectively. Each will “automatically” find the right Application Pool to run under.
Conclusion
You can develop and test ASMX and WCF Web Services for .Net 3.5 (and earlier) and for .Net 4.0 on the same machine with Visual Studio 2008 and Visual Studio 2010 Side by Side
Continued from Part 2.
So now for the second scenario. Hosting WPF controls in legacy containers. Well let’s start with hosting a WPF control in a Windows Form. Then we’ll have a bash at hosting a WPF control in an unmanaged application (hee, hee).
Here is the source code for the three projects described in this post.
Hosting WPF Controls in a Windows Form
Hosting WPF Controls in a Windows Forms application is quite straightforward. The key is the ElementHost control in the Systems.Windows.Forms.Integration namespace which is a Windows Forms Control and can reference any WPF UIElement as its Child property.
You will find a walkthrough in MSDN here and there are a number of good posts on the subject. But just for the sake of completion, I created 3 projects here which demonstrate: implementation of the PolygonControl as a WPF UserControl, hosting of the control in a WPF application and then hosting it in a Windows Forms Application.
Let’s start with the PolygonControl in WPF (project WpfControlLibrary in the source code).
Here is my XAML:
<UserControl x:Class="WpfControlLibrary.PolygonControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="300"
Width="300"
Loaded="UserControl_Loaded">
<Canvas x:Name="canvas" MouseDown="canvas_MouseDown">
<Rectangle Name="rectangle"
Width="{Binding ElementName=canvas, Path=ActualWidth}"
Height="{Binding ElementName=canvas, Path=ActualHeight}"
Fill="White"></Rectangle>
<Polygon x:Name="polygon"
Fill="Yellow">
</Polygon>
</Canvas>
</UserControl>
And the code behind:
public partial class PolygonControl : UserControl
{
public PolygonControl()
{
InitializeComponent();
}
public event EventHandler ClickIn;
public event EventHandler ClickOut;
int m_nSides;
public int Sides
{
get { return polygon.Points.Count; }
set
{
if (value >= 3 && value <= 100)
{
m_nSides = value;
CalculatePoints(canvas.ActualWidth, canvas.ActualHeight);
}
}
}
private void CalculatePoints(double width, double height)
{
Point ptCenter = new Point();
double dblRadiusx = width / 2;
double dblRadiusy = height / 2;
double dblAngle = 3 * Math.PI / 2; // Start at the top
double dblDiff = 2 * Math.PI / m_nSides; // Angle each side will make
ptCenter.X = width / 2;
ptCenter.Y = height / 2;
PointCollection points = new PointCollection(m_nSides);
// Calculate the points for each side
for (int i = 0; i < m_nSides; i++)
{
points.Add(new Point(
dblRadiusx * Math.Cos(dblAngle) + ptCenter.X,
dblRadiusy * Math.Sin(dblAngle) + ptCenter.Y));
dblAngle += dblDiff;
}
polygon.Points = points;
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
Sides = 3;
}
private void canvas_MouseDown(object sender, MouseButtonEventArgs e)
{
if (e.Source == polygon)
{
polygon.Fill = Brushes.Red;
if (ClickIn != null)
ClickIn(this, EventArgs.Empty);
}
else
{
polygon.Fill = Brushes.Yellow;
if (ClickOut != null)
ClickOut(this, EventArgs.Empty);
}
}
}
Wow, that was easy. We’ve come a long way since ATL, wouldn’t you say?
Worthy note in this code are the following:
- In order to be able to determine where the mouse was “mouse downed” I used good old routed events.
- A Rectangle occupies the whole Canvas, and the Polygon lies over it.
- MouseDown is a bubbling event so we can catch it firing at the Canvas. In the handler of the Canvas we can use e.Source to determine whether the click is happening in the polygon or in the rectangle.
- In order to get the rectangle to stretch over the Canvas I used binding to bind its dimensions to those of the Canvas.
- Here again we see the beauty of a retainable graphics system. As soon as I set the PointCollection on the Polygon, WPF updates the screen (no need to manually call FireViewChange like in ATL).
As for the hosting code in WPF and in Windows Forms, they are almost identical (once you add the ElementHost in the Windows Form to host your control). See the WpfUseControl and WfUseControl projects respectively in the source code for this post.
And now, for the real challenge:
Hosting WPF Controls in an Unmanaged Application
Unfortunately, to the best of my knowledge there is no easy way to host a WPF UserControl in an unmanaged application. Yes, its true that you can expose the WPF UserControl as a COM object (you know the routine, define an interface, derive the control from the interface, add the Guid attribute and the ComVisible attribute, sign the application and check the “Register for COM interop” on the Build tab of the project properties).
This enables you to create instances of the control and call its methods from C++, but doesnt give you any GUI. Even if you make your control a Window instead of a UserControl, you wont see anything (I assume because you dont have an Application object) but most important, your new COM component is just NOT an ActiveX.
An ActiveX is a special sort of COM that supports ‘OLE’ - object linking and embedding into a container through a set of unmanaged COM interfaces. The only interfaces your COM component supports are those you explicitly made visible.
I say “there is no easy way” because I assume it is possible to write a wrapper COM component that implements the OLE interfaces and provides the glue between the OLE container and a UIElement, but to the best of my knowledge it hasn’t been written yet.
Continuing from Part 1.
We are still discussing the first scenario for WPF Interop, namely using COM and Windows Forms User Controls in a WPF application. Here I will be giving some examples. In the next post I will discuss the second scenario – hosting a WPF control in a Windows Form application.
You can download all the source code for Part 1 and Part 2 of this article here.
Please note: All projects were prepared with Visual Studio 2010 Beta 1.
WPF Application using a Simple COM Object (ATL)
I used ATL to create a Simple COM Object using these steps.
- Add a new project of type ATL Project (under “Other Languages”. Interesting how the demoted king of object-oriented languages is now just “another” language).
- Call it SimpleControl.
- Check “Allow merging of proxy/stub code”.
- Use Add Class to add an “ATL Simple Object” and name it Calculator.
- Enable Connection Points on the new object before completing the Wizard.
- Add a Multiply method (with 2 double operands and returning a double) to the ICalculator interface.
- Fill in the implementation of the Multiply method in Calculator.cpp.
- Add another method called MultipleAsync (with 2 double operands returning void). This will be an asynchronous call to Multiply.
- Add a MultiplyCompleted event in the ICalculatorEvents interface using the Wizard. This step in the Wizard fails to add the method to the events interface in the .idl file, so you have to add it yourself.
- Implement the CProxy_ICalculatorEvents interface in _ICalculatorEvents_CP.h using the Wizard. This step in the Wizard also fails and does not generate the Fire_MultiplyCompleted method. So you will have to write it yourself or copy a similar routine from elsewhere and make the required modifications.
- My implementation of MultiplyAsync involves a background thread which fires the MultiplyCompleted event after a 3 second sleep and the calculation is complete.
Now we have that in place, lets run tlbimp on the SimpleControl.dll, to create the RCW SimpleControlLib.dll.
Create a console application (as the Calculator control has no GUI there was no point making the same a WPF application) and add a reference to the SimpleControlLib.dll.
We can now consume the COM control through the managed wrapper as follows:
using SimpleControlLib;
namespace ConsoleCalculatorApp
{
class Program
{
static void Main(string[] args)
{
Calculator calculator = new CalculatorClass();
double result = calculator.Multiply(3, 5);
Console.WriteLine(result);
calculator.MultiplyCompleted += calculator_MultiplyCompleted;
calculator.MultiplyASync(5, 6);
Console.ReadLine();
}
static void calculator_MultiplyCompleted(double result)
{
Console.WriteLine(result);
}
}
}
WPF Application using a Windows Form User Control
A good example of using a Windows Form User Control is the one shown in the Walkthrough: Hosting an ActiveX Control in Windows Presentation Foundation. Though the title of the walkthrough indicates that it describes a method to host ActiveX controls in a WPF application, in fact, it does so by creating a Windows Form User Control that hosts the ActiveX control and then hosts the User Control in the WPF application using the WindowsFormsHost, so it shows how to host any Windows Forms UserControl in a WPF application.
To demonstrate the technique in the walkthrough we will need an ATL ActiveX Control (once called “Full Control”) to work with.
This is where the Polygon sample from the MSDN documentation comes in. As I bemoaned in an earlier post, I wasn’t successful in running that code in VS 2008 nor did the ATL wizard work in Visual Studio 2010 when I tried to build it myself guided by the tutorial. In the end I hand coded some of the mixing text and the result is the Polygon project in the code for this article.
I am including in the source code for the article two more projects:
- WfPolygonUserControlLibrary which is the Windows Forms User Control Library which defines WfPolygonUserControl that wraps the ActiveX.
- WpfPolygonAppWithUserControl which demonstrates the use of the WfPolygonUserControl in a WPF project.
As previousltymentioned, this approach is simple to implement (there is no need to call aximp on the command line. All you need to do is to select Choose Items on the Tool Box and wait for 10 minutes to add the ActiveX component to the Tool Box from the COM tab).
However, in a way, it is inefficient because in order for Visual Studio to add the ActiveX control to the Tool Box it needs to generate two assemblies. One is the RCW (with the name Interop.PolygonLib.dll) and a second (called AxInterop.PolygonLib.dll) contains a Control that wraps the RCW .
The next section demonstrates the approach that I prefer which is to use tlbimp and host the control it creates directly in the WindowsFormsHost without building an intermediate WfPolygonUserControl.
WPF Application using an ATL ActiveX Object
I ran tlbimp on the Polygon.dll dll to create AxPolygonLib.dll (UserControl) and PolygonLib.dll (RCW).
Then I created a new WPF project called WpfPolygonApp to which I added the following references:
- WindowsFormsIntegration (for the WindowsFormsHost)
- PolygonLib.dll
- AxPolygonLib.dll
This is the code for my window, in which you can see the WindowsFormsHost in action.
<Window x:Class="WpfPolygonApp.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wfi="clr-namespace:System.Windows.Forms.Integration;assembly=WindowsFormsIntegration"
xmlns:uc="clr-namespace:AxPolygonLib;assembly=AxPolygonLib"
Title="Window1"
Height="300"
Width="300">
<Grid>
<wfi:WindowsFormsHost>
<uc:AxPolyCtl x:Name="polyCtl"
ClickIn="polyCtl_ClickIn"
ClickOut="polyCtl_ClickOut" />
</wfi:WindowsFormsHost>
</Grid>
</Window>
And this is the code behind:
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
void polyCtl_ClickIn(object sender, _IPolyCtlEvents_ClickInEvent e)
{
polyCtl.Sides++;
polyCtl.FillColor = System.Drawing.Color.Green;
}
void polyCtl_ClickOut(object sender, _IPolyCtlEvents_ClickOutEvent e)
{
polyCtl.Sides--;
polyCtl.FillColor = System.Drawing.Color.Red;
}
}
As you can see, I followed the example in Step 7 of the ATL Tutorial which builds an html page for the polyCtl control and implements event handlers such that when clicking inside the polygon the number of sides increases and when clicking outside, the number of sides decreases.
Here I also added a change of the FillColor to demonstrate use of the Stock Property that we added in the ATL control (Step 2 in the ATL Tutorial).
This concludes our discussion on consuming COM objects (simple UI-less controls and ActiveX controls) and Windows Forms Controls in WPF Applications.
In the next post I will review the reverse – using a WPF control in a Windows Forms application.
OK. I finally managed to create the sample ATL called Polygon from the ATL Tutorial on MSDN using the Visual Studio 2010 Beta 1. The problems I mention in the previous post remain; I simply hand-coded the text that the ATL wizard failed to create (well, maybe simply is not appropriate here).
Anyway, back to interop.
We have two scenarios to examine:
- WPF using COM objects and Windows Forms User Controls
- Windows Forms using WPF controls
Let’s start with the first.
In the rest of this post I will describe the recipes for a number of cases. In the following post I will provide sample applications for each. The examples include the implentations of COM objects with ATL and the WPF applications that consume them.
The source code for those samples is here.
WPF Application using a Simple COM Object
When I say simple, I mean one that has no UI. All you need to do in this case is to run the command line utility tlbimp.exe followed by one argument – the name of the dll that contains the COM object. This will create a new dll with the same name as the original, appended by Lib (tlbimp MyCom.dll will create MyComLib.dll). This will create a RCW (Runtime Callable Wrapper), that is proxies for your COM objects within the MyComLib namespace. You can instantiate these classes with the new operator and consume them from any .Net code - in particular from a WPF application.
WPF Application using a Windows Form User Control
A WPF application can use a Windows Form User Control using the WindowsFormHost class from the System.Windows.Forms.Integration namespace. WindowsFormHost is a FrameworkElement which makes it a welcome member to any WPF application and at the same time, it takes any System.Windows.Forms.Control as its content (through its Child property).
WPF Application using an ATL ActiveX Object
An ActiveX is a COM object, of course, but it also implements interfaces that usually give it a graphic user interface and the ability to be hosted in other applications (the Ol’ OLE). Those interfaces work with COM containers, but not with Windows Form or WPF windows. If you want to host the ActiveX and give it GUI, it needs to be wrapped in a Windows Form User Control. Such controls can then be hosted in a WPF application as described above.
The simplest way to create a Windows Form User Control wrapper for an ActiveX is to call the command line utility aximp.exe followed by one argument – the name of the dll that contains the COM object. This will create two new dlls. One will be the same RCW produced by tlbimp.exe (as described above. So for MyCom.dll the RCW will be called MyComLib.dll). The other has the same name as the RCW with a prefix of Ax (AxMyComLib.dll). This second dll defines a UserControl in the namespace AxMyCom and it references (depends on) the RCW.
Interestingly enough the MSDN documentation offers another option. Create a new Windows Forms User Control to contain the object in AxMyComLib. Then embed the result in a WindowsFormHost element in the WPF application. One small advantage of this approach is that you dont need to run aximp or tlbimp. Instead, you add your COM component to the ToolBox (by right clicking on the ToolBox, selecting Choose Items and selecting the component from the COM tab).
You can then drag the COM component from the ToolBox onto the UserControl design surface. When you do this, Visual Studio will automatically create the two dlls produced by aximp (with an addition of the string “interop.” in the names of each) and reference them in your project. (Actually it references only the AxMyComLib.dll assembly and issues a warning proposing you to add a reference to the interop dll too).
The disadvantage of this approach is that you add an additional UserControl wrapper between the WPF application and the COM implementation.
In the next post, we will see each of these principles in code samples.
I am working on a post or two on the subject of WPF Interop with ATL and Windows Forms.
I would like to demonstrate:
- ATL Simple Control in a WPF Window
- ATL ActiveX in a WPF Window
- Windows Form user control in a WPF Window
- WPF control in a Windows Form
However, I encountered an unexpected setback: Its very difficult to create ATL projects with Visual Studio 2010 Beta 1!
- The ATL Wizard doesnt add implementations in the control to methods you add to the interface.
- When implementing connection points, methods of the event interface are not added to the idl, nor are they implemented in the proxy code for the event interface.
Actually, there have been problems with the ATL Wizard since the CTP (see here).
So, I thought, I will overcome the problem by downloading the completed samples from the MSDN. I tried the Polygon sample and I got a second surprise.
These samples were written for Visual Studio 2005. I loaded the Polygon sample into Visual Studio 2010 Beta 1 and the conversion failed : (
Well, I dont have a copy of 2005 (I keep VC 6.0 because there are so many things it does better than any of the later IDEs). So I tried Visual Studio 2008.
This time the conversion worked, but I couldnt get the project to compile. Whatever I did it would report “Skipping compile” and do nothing ….
Well, I intend to resolve this problem in the next few days, and then demonstrate the interoperation with WPF…
So, whats the difference, and when should I use each?
I am referring of course to the difference between the behavior of these two XAML elements:
<Button Background="{StaticResource myBrush}">Static Resource</Button>
<Button Background="{DynamicResource myBrush}">Dynamic Resource</Button>
Where “myBrush” is defined as a resource in a scope that contains the buttons.
The Difference
Static resources are evaluated at compile time. Dynamic resources are evaluated at runtime, when they are needed. That includes whenever they are replaced with new objects.
Note that if the resource itself is derived from Freezable, changes to the object will change the UI regardless of whether you used static or dynamic resource binding. (Brush, Animation and Geometry are all Freezable).
The following demonstration should make this clear.
Demonstration
The following code demonstrates this behavior:
XAML:
<Window x:Class="DynamicStyles.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1"
Height="300"
Width="300"
Loaded="Window_Loaded">
<Window.Resources>
<SolidColorBrush Color="Red"
x:Key="myBrush"></SolidColorBrush>
</Window.Resources>
<StackPanel>
<Button Background="{StaticResource myBrush}">Static Resource</Button>
<Button Background="{DynamicResource myBrush}">Dynamic Resource</Button>
<Button Click="ChangeBrush_Click">Change brush</Button>
<Button Click="ChangeResource_Click">Change resource</Button>
</StackPanel>
</Window>
C#
using System;
namespace DynamicStyles
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
SolidColorBrush myBrush;
SolidColorBrush anotherBrush;
private void Window_Loaded(object sender, RoutedEventArgs e)
{
myBrush = Resources["myBrush"] as SolidColorBrush;
anotherBrush = new SolidColorBrush(Colors.Blue);
}
private void ChangeBrush_Click(object sender, RoutedEventArgs e)
{
if (myBrush.Color == Colors.Red)
{
myBrush.Color = Colors.Blue;
}
else
{
myBrush.Color = Colors.Red;
}
}
private void ChangeResource_Click(object sender, RoutedEventArgs e)
{
if (FindResource("myBrush") as SolidColorBrush == myBrush)
{
Resources["myBrush"] = anotherBrush;
}
else
{
Resources["myBrush"] = myBrush;
}
}
}
}
In this application, the top two buttons have no function - they just bind their Background property to a resource whose resource key is “myBrush”. The first button binds statically, the other dynamically.
The myBrush member is initialized in the Window_Loaded handler to the resource whose resource key is “myBrush”.
The third button is linked to ChangeBrush_Click which changes the Color property of myBrush, toggling between Red and Blue.
The fourth button is linked to ChangeResource_Click which changes the entry in the Resource dictionary, toggling between the original brush and another that’s blue.
If you run the application and press the third button, both of the two top buttons change their color because myBrush is a Freezable object and changes in its properties are propagated to the underlying presentation layer.
If you then press the fourth button, only the second button changes color. This is because static resources are resolved once, at compile time. Dynamic resources, on the other hand, are evaluated at run time, in particular when the resource entry to which they point is replaced with another.
If you now press the third button again the background of the first button (Static Resource) changes but not the second (Dynamic Resource). This is because the second button, being dynamically resolved, no longer points to myBrush which is the object that is being modified by ChangeBrush_Click.
Play around with it to see.
Choosing between DynamicResource and StaticResource
- Static Resource requires less run time CPU so it will usually be your first choice.
- An exception to this rule is if you want to speed up the loading process of the application. Static resources are all assigned when the application loads. If there are many such assignments, loading may be slow. Dynamic resources, on the other hand, will only be evaluated when needed, so the initial loading of your main window may be quicker with dynamic resources.
- If your resources are unknown at compile time, use Dynamic Resource. There are two typical situations when this might be the case:
- You want to change the theme of your application at run time (for instance you might offer the user the option to personalize the application). In this case you can modify resource dictionary entries in the application and all dynamic resources will change their look.
- You are binding to system resources like current Windows colors or fonts. If you want your application to react to changes in the system settings associated with these resources, they should be bound dynamically.
Recently, a message with this caption started popping up when I open some of my projects with Visual Studio 2008.
The detailed text of the message is:
The <Project Name> project file has been customized and could present a security risk by executing custom build steps when opened in Microsoft Visual Studio. If this project came from an untrustworthy source, it could cause damage to your computer or compromise your private information.
- Load Project for Browsing
- Load Project Normally
A “More Details” button follows. When I press it I get a message box with:
An item referring to the file "" was found in the project file <Project File Path>. Since this file is located in a system directory, root directory or network share it could be harmful to write to this file.
Needless to say, I haven’t (knowingly) added custom build steps to these projects, nor had I knowingly add references to a file named “”.
The most annoying thing about this warning is that there seems to be no way to turn it off for next time. It appears everytime I open the project.
I googled this and there are many complaints, but no answers that I could find. (Some developers reinstalled Visual Studio 2008 and the problem didnt go away.)
I managed to work around the problem in every case I encountered it, and here is how. But beware, I dont know real cause of the problem or what side effects this change may have, so take my tip at your own risk : )
Workaround
- Open the project.
- When the Security Dialog comes up, press OK to load the project.
- In Solution Explorer right click on the project and select “Unload Project”
- Right click again on the project and select “Edit <Project Name>.csproj”
- In the first PropertyGroup element under the root element, delete the <ProjectGuid> element.
- Save the project file.
- In Solution Explorer, right click again on the project and select “Reload Project”
Hey Presto, the security dialog box doesn’t appear any more.
To Undo
The ProjectGuid is still stored in your solution file for the project.
An easy way to get it back into your project is as follows.
- Follow steps 1-4 in the Workaround.
- Add an empty <ProjectGuid></ProjectGuid> element anywhere (XML valid) in the first PropertyGroup element.
- Load the project again.
- Select Save All (or press OK on the prompt to save the project when you close the solution).
This restores the original ProjectGuid value inside the element you added (and with it, the annoying security dialog).
Afterword
As I cant explain this workaround, it is not a solution as far as I am concerned. I will continue looking out for a proper one, and if you know or find one meanwhile, please comment and share it !
Download the code for this article here.
In this post I am going to describe a simple video player application written with WPF.
It is not a groundbreaker, but it provides easy-to-copy demonstrations of:
- Databinding between a collection of objects and a control.
- Writing a DataTemplate for ListBox.ItemTemplate
- Databinding between properties of two controls.
- Controlling the MediaElement, in particular how to create a Position slider that works.
- Writing a ControlTemplate for Button.Template
- Animating a property of a Button.
This is what we will build:
Application Layout
Let’s start by laying out the application in the XAML Designer:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" />
<ColumnDefinition Width="500" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<ListBox Name="listBox">
</ListBox>
<Slider Name="sliderVolume"
Minimum="0"
Value="0.5"
Maximum="1">
</Slider>
</StackPanel>
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border Grid.Row="0"
BorderBrush="Aqua"
BorderThickness="2"
Padding="2">
<MediaElement Name="player">
</MediaElement>
</Border>
<Grid Margin="10"
Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Name="tbPosition"
Grid.Column="0"
Margin="2"
VerticalAlignment="Center">00:00:00</TextBlock>
<Slider Name="sliderPosition"
Grid.Column="1"
Margin="2"></Slider>
<TextBlock Name="tbDuration"
Grid.Column="2"
Margin="2"
VerticalAlignment="Center">00:00:00</TextBlock>
</Grid>
</Grid>
</Grid>
As you can see, we have a Grid with two columns of fixed size.
The left column hosts a stackpanel with a ListBox on the top and a Slider on the bottom. The ListBox will present all the files in our video folder, and the Slider will control the volume.
The right column hosts another Grid, with two rows. The top row hosts a MediaElement within a Border. On the second row we have the Position control Slider which I placed in a Grid with 3 columns. The Slider itself is in the middle column, with the Position as text in the left column and the Duration as text in the right column.
Databinding Between a Collection of Objects and a Control
I would like the ListBox to present the files in the video folder. This can be done quite easily by binding to the result of a LINQ query in code behind (in the Loaded event of the Window).
string videoFolder = Properties.Settings.Default.VideoFolder;
private void Window_Loaded(object sender, RoutedEventArgs e)
{
listBox.ItemsSource =
from string fileName in Directory.GetFiles(videoFolder)
where Path.GetExtension(fileName) == ".wmv"
select new FileInfo(fileName);
}
As you can see, I read the videoFolder location from an application setting.
We are binding the ListBox to a list of FileInfo items. Interestingly, this binding works even without setting the DisplayMemberPath property of the ListBox. (The ListBox displays the FullName string property for each item, by default).
Writing a DataTemplate for ListBox.ItemTemplate
In order to control what gets displayed in the ListBox, and, indeed, in order to control every aspect of how that list will look, lets go back to the XAML and write the following DataTemplate for the ListBox:
<ListBox Name="listBox">
<ListBox.ItemTemplate>
<DataTemplate>
<Button Width="170"
Margin="2"
Background="White"
HorizontalContentAlignment="Left"
Click="Button_Click"
Content="{Binding Path=Name}"
ToolTip="{Binding Path=Length}">
</Button>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
This template is quite straightforward. The ListBox will now create a Button for each FileInfo to which it is bound. I chose a button rather than a Rectangle because I want to be able to handle a Click event (with Button_Click). As we will see, this handler will Play or Stop the MediaElement’s playback of the file associated with this Item.
Apart from being able to control the look of that Button the significant benefit of using the DataTemplate is that we can decide which properties of each item should be used in the representation of that item. In this case, I am binding the Name of the FileInfo (rather than the FullName) to the Content property of the Button and the Length (the size in bytes) to the ToolTip property. Yes, I know the length in bytes of the file is of no use to anyone, but it demonstrates a principle, I think.
Databinding Between Properties of Two Controls
Next, lets set up the MediaElement – starting with the XAML:
<MediaElement Name="player"
LoadedBehavior="Manual"
MediaOpened="mediaElement_MediaOpened"
MediaEnded="mediaElement_MediaEnded"
Volume="{Binding ElementName=sliderVolume, Path=Value}">
</MediaElement>
The first thing to notice here is that the LoadedBehavior is set to Manual. This is the mode that allows us to call Play and Stop on the MediaElement in code. I am adding two event handlers that will be fired when a file is opened and when it stops playing, respectively. We will use those in just a moment.
The value given to Volume is the one that contains the data binding. In this case I am using the ElementName option to specify that the source of the data will be another (named) control and not any business object or unnamed control in the logical tree. The Path value of “Value” indicates that the binding source is the Value property of the sliderVolume Slider.
Its important here to make sure that sliderVolume is indeed setup correctly to provide values between 0 and 1 as this is the range of the Volume property.
Controlling The MediaElement
You probably already noticed that I didnt provide a Play or Stop button in this application. Instead, you play a video by clicking on its button in the file list. You stop the video playing by clicking on the same button again. Not perfect, but its one way to go.
I implemented this in the Button_Click event handler as follows:
private void Button_Click(object sender, RoutedEventArgs e)
{
Button prevButton = player.Tag as Button;
Button button = sender as Button;
FileInfo fileInfo = button.DataContext as FileInfo;
// If a file is playing, stop it
if (prevButton != null)
{
player.Tag = null;
player.Stop();
prevButton.Background = Brushes.White;
// if the one thats playing is the one that was clicked -> don't play it
if (prevButton == button)
return;
}
// Play the one that was clicked
player.Tag = button;
player.Source = new Uri(fileInfo.FullName);
player.Play();
button.Background = Brushes.Aqua;
}
First, note the third line of the method which demonstrates that the DataContext property of each button has been inherited from the ListBoxItem that contains it, and is the business object of type FileInfo that was bound to that item.
This allows us to readily derive the FullName (path) of the file that needs to be played.
Second, note the use of the Tag property of MediaElement (of any FrameworkElement, actually) to store the button that is bound to the currently playing file (or null if none is playing). This is needed so I can change the background of the buttons as files are played and stopped.
Clearly, I didn’t have to use the ‘Tag’ property. I could just as well have used a member of the Window class, but I think this approach is neater and more object oriented.
Creating a Position Slider That Works
It was easy to get that Volume control Slider working because MediaElement has a dependency property called VolumeProperty, but Position is not a dependency property of MediaElement. Moreover, for Position we need two-way binding rather than one way because the Slider should show the progress of the MediaElement as it plays a file.
I would imagine that the reason for not defining a dependency property for Position is to disuade us from binding it to Slider values. Seek operations are CPU intensive and mostly unnecessary during the time you are dragging a Slider.
So, instead, the recommended approach is polling.
Lets add a DispatcherTimer member to the Window and add the following to the Window_Loaded handler.
timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromSeconds(1);
timer.Tick += new EventHandler(timer_Tick);
This is a first draft of the time_Tick routine:
void timer_Tick(object sender, EventArgs e)
{
sliderPosition.Value = player.Position.TotalSeconds;
}
We call the Start and Stop methods of the timer in the mediaElement_MediaOpened and mediaElement_MediaEnded handlers respectively.
This code efficiently updates the Slider position as the player progresses.
Now for the Seek feature.
First we need to add some event handlers to the sliderPosition object in XAML, like so:
<Slider Name="sliderPosition"
Grid.Column="1"
Minimum="0"
Margin="2"
ValueChanged="sliderPosition_ValueChanged"
Thumb.DragStarted="sliderPosition_DragStarted"
Thumb.DragCompleted="sliderPosition_DragCompleted"
></Slider>
These two routed events of Track, DragStarted and DragCompleted, are not too conspicuous at first, but they really help solve the Seek problem well (these are Thumb dragging events, not to be confused with Drag and Drop, mouse related events).
Here is the implementations of the handlers (first draft):
bool isDragging = false;
private void sliderPosition_ValueChanged(
object sender, RoutedPropertyChangedEventArgs<double> e)
{
TimeSpan ts = TimeSpan.FromSeconds(e.NewValue);
tbPosition.Text =
String.Format("{0:00}:{1:00}:{2:00}",
ts.Hours, ts.Minutes, ts.Seconds);
}
private void sliderPosition_DragStarted(
object sender, DragStartedEventArgs e)
{
isDragging = true;
}
private void sliderPosition_DragCompleted(
object sender, DragCompletedEventArgs e)
{
isDragging = false;
player.Position = TimeSpan.FromSeconds(sliderPosition.Value);
}
As you can see, when the Slider position changes, I only update the tbPosition text, so while dragging the Slider there is no attempt to Seek. Instead, the Seek occurs once when the drag is completed.
This implementation doesnt allow you to see a preview of the video as you drag, but it is efficient and responds well to the user.
There is one more detail that shouldn’t be overlooked. While the slider is being dragged we need to cancel the effect of the timer. This is why I defined and manage the isDragging member. This is the updated timer_Tick routine:
void timer_Tick(object sender, EventArgs e)
{
if (!isDragging)
{
sliderPosition.Value = player.Position.TotalSeconds;
}
}
Before we finish up with some styling in XAML, let me just show you the mediaElement_MediaOpened handler which is called whenever the MediaElement opens a new file for playing:
private void mediaElement_MediaOpened(object sender, RoutedEventArgs e)
{
if (player.NaturalDuration.HasTimeSpan)
{
TimeSpan ts = player.NaturalDuration.TimeSpan;
sliderPosition.Maximum = ts.TotalSeconds;
sliderPosition.SmallChange = 1;
sliderPosition.LargeChange = Math.Min(10, ts.Seconds / 10);
tbPosition.Text = String.Format("00:00:00");
tbDuration.Text = String.Format("{0:00}:{1:00}:{2:00}",
ts.Hours, ts.Minutes, ts.Seconds);
}
timer.Start();
}
In this event we can discover the duration of the file and set the scale for the Slider and the value that is written to the tbDuration TextBlock.
OK, now for some fun with templates and animation…
Writing a ControlTemplate for Button.Template
First, lets replace the insides of the Button inside the ListBox DataTemplate.
The ContentPresenter must stay, thats the placeholder for whatever content we are placing in the button – but everything else can be replaced. There is nothing to exciting here about this Border with its rounded edges, but of course, there is no limit to what you could do instead.
Note the use of the TemplateBinding syntax which allows the template to be configured through properties on the control (Button) itself. In case you were wondering, the ContentPresenter should also really include a TemplateBinding expression binding it to the Content property of the Button. As it so happens, that binding is implicitly present and doesnt need to be written in explicitly.
<Button Width="170"
Margin="2"
Background="White"
HorizontalContentAlignment="Left"
Click="Button_Click"
Content="{Binding Path=Name}"
ToolTip="{Binding Path=Length}">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border CornerRadius="5"
BorderBrush="Black"
Background="{TemplateBinding Background}"
BorderThickness="1"
Padding="2">
<ContentPresenter>
</ContentPresenter>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Button.Style>
</Button>
Styles can be used to set the value of any property of their target control, but this one has only one purpose - to set the Template property of the Button to a new ControlTemplate. ControlTemplates offer a much higher level of control over the internals of a Control, allowing us to completely redesign the ones provided with WPF.
Typically Styles are defined in a ResourceDictionary in the scope of an Application, Window or a container control. The reason for this is to allow Styles to be applied to more than one control and to help us build applications that are consistent in their style and easy to restyle. In this case, I have no other use for this particular style, so my basic instinct to encapsulate wherever possible got the upper hand. I therefore used the Element-Property technique in XAML to specify the Style within this Button which is the only control that uses it.
Animating a Property of a Button
When you try the application out for yourself you will no doubt notice that when you press any of the buttons in the playlist the button reacts by slowly expanding in the horizontal direction and shrinking back to its normal size.
In Windows Forms, we would have had to work quite hard to implement this effect, and the implementation would probably really clutter up our code.
In WPF, as I am sure you know, animations (at least simple ones) can be added quite efficiently in XAML. Animations can be activated using an EventTrigger which is contained in a Trigger collection. In this demo I will attach the TriggerCollection to the ControlTemplate that we already designed.
<ListBox Name="listBox">
<ListBox.ItemTemplate>
<DataTemplate>
<Button Width="170"
Margin="2"
Background="White"
HorizontalContentAlignment="Left"
Click="Button_Click"
Content="{Binding Path=Name}"
ToolTip="{Binding Path=Length}">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border CornerRadius="5"
BorderBrush="Black"
Background="{TemplateBinding Background}"
BorderThickness="1"
Padding="2">
<ContentPresenter>
</ContentPresenter>
</Border>
<ControlTemplate.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard
Storyboard.TargetProperty="Width">
<DoubleAnimation From="170"
To="180"
Duration="0:0:0.5"
AutoReverse="True">
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Button.Style>
This animation increases the Width property over 0.5 seconds (and then reverses the operation due to the AutoRevers=True setting.
Now compile the project and run (dont forget to set the VideoFolder setting in App.Config to point to a folder with WMV files).
Enjoy!
Summary
In this article we reviewed samples of the following WPF features:
- Writing a DataTemplate for ListBox.ItemTemplate
- Databinding between properties of two controls.
- Controlling the MediaElement, in particular how to create a Position slider that works.
- Writing a ControlTemplate for Button.Template
- Animating a property of a Button.
Feel free to send me feedback on this article – and to use the code in whole or portion for your own needs, as you see fit.
You can download the code from here.