C H A P T E R  10

images

Application Scripting

One of the most common uses of the DLR and dynamic languages is in the area of application scripting. Application scripting allows users to control an application's behavior by writing scripts. For example, users might write scripts to automate routine tasks. They might use scripts to add new features or to customize existing functionalities. They might write scripts that integrate an application with other applications. The usage scenarios are many and the benefits are real. In this chapter, we will look at this important use of the DLR and dynamic languages. Specifically, we will develop a fun application that has balls bouncing around, and we will open up part of the application for users to customize. The way we open up the application is by exposing its object model for users to script. Users or third-party vendors can write custom IronPython code to script the object model. Our application will use the DLR Hosting API to load and run those custom IronPython scripts.

Ball World

The application we will build in this chapter is called Ball World. You can find the complete source code in the BallWorld project of this chapter's code download. Figure 10-1 shows a snapshot of what Ball World looks like when it runs.

The Ball World application is a simple, standalone program that simulates a world of balls with different colors and sizes. The world is two-dimensional and is bounded by four invisible walls. The world has an initial state that determines what balls are in the world and specifies the colors, sizes, speed, and positions of the balls. It is the initial state that we will open up for users and third-party vendors to script. Unlike the world we live in, the world of balls does not have gravity that constantly pulls the balls downward. A ball in our simulated world moves in a straight line until it hits one of the four bounding walls or another ball. When a ball hits any of the four walls, it bounces back. When two balls collide, they assert a force on each other and hence change each other's direction of movement. The mass of a ball is proportional to its size. So when a big ball collides with a smaller ball, the bigger one will have a less dramatic change in its direction of movement than the smaller one.

images

Figure 10-1. The Ball World Application

Ball World is a WPF (Windows Presentation Foundation) application. Don't worry if you have little or no experience with WPF. The focus of this chapter is on application scripting, and WPF just happens to be the underlying framework our example application is built on. We will stay focused on application scripting and will only get into the specifics of the WPF part of the Ball World application as necessary.

Besides WPF, the other component the Ball World application is built on is an open source project called Farseer Physics. Farseer Physics is a library that provides many cool features people normally need when they develop game applications that need to simulate the physical world. In our case, we will use only a very limited subset of the features Farseer Physics provides for detecting the collisions between two balls or between a ball and a wall. You don't need to have any prior experience with Farseer Physics or in game development. This chapter will not have too detailed a discussion on those subjects. Our focus is on application scripting and I chose to use the Farseer Physics library only to make Ball World more fun to play with.

Software Requirements

To run the Ball World application you need the compiled binaries of the Farseer Physics library. Here are the steps for setting up the Farseer Physics library for Ball World.

  1. Download the Farseer Physics source code from the CodePlex web site at http://farseerphysics.codeplex.com/. The version of Farseer Physics I used for developing the Ball World application is 2.1.3. The later version 3.0 is different from version 2.1.3 in substantial ways and is not backward-compatible. Be sure to download version 2.1.3 to follow along this chapter's code examples. For each version of Farseer Physics, there are different packages you can download for different runtime environments. Because our runtime environment is WPF, you should download the package called Farseer Physics 2.1.3 Class Library. I will assume that you unzip the downloaded package into the folder C:Farseer Physics 2.1.3 Class Library. If you use a different folder, you'll need to adjust the rest of the setup steps in this section accordingly.
  2. Open the FarseerPhysics.csproj file in Visual Studio C# 2010 Express. Because this file is for an older version of Visual Studio, you'll be prompted to convert it to a format that Visual Studio C# 2010 Express understands. Simply go through that conversion wizard and have the FarseerPhysics.csproj file converted. After the conversion is done, you'll see a project called FarseerPhysics in the Solution Explorer of Visual Studio.
  3. Build the FarseerPhysics project. The compiled binaries will be placed in C:Farseer Physics 2.1.3 Class LibraryinDebug. Copy all the files in that folder to C:ProDLRlibFaseerPhysicsDebug and you are done.

Application Architecture

As mentioned earlier, Ball World is a WPF application. In the world of WPF, there is a popular pattern called MVVM (Model-View-ViewModel) for architecturing applications. A detailed discussion of the MVVM pattern is beyond the scope of this chapter and might not be of interest to readers who don't base their application's user interface on WPF. I'll cover MVVM just enough here so you know how the Ball World application is structured in the context of the MVVM pattern.

MVVM is a design pattern for structuring the user interface layer of an application. If you've built UI applications, you are most likely familiar with the MVC (Model-View-Controller) pattern used in many non-WPF UI applications. The MVVM pattern is similar. For WPF applications, people use MVVM instead of the more traditional MVC design pattern because MVVM takes advantage of capabilities such as data binding that WPF provides. As its name suggests, the MVVM pattern basically consists of three parts: model, view, and viewmodel. Model is made up of classes that represent the objects of the application's domain. View defines the look and feel of the application. ViewModel is the part that uses WPF data binding to bridge the view and the model. If you compare MVVM to the MVC pattern, you'll see the similarities. The MVC pattern also consists of three parts: model, view, and controller. The model part of MVVM corresponds to the model part of MVC. The view part of MVVM corresponds to the view part of MVC. And the viewmodel part of MVVM corresponds to the controller part of MVC.

For our Ball World application, I adopted the MVVM pattern but simplified things by combining the model and viewmodel into a single object model. This is okay for a simple application like Ball World. For a more complex application, the general practice would be to implement the full MVVM pattern. The single object model we have for Ball World contains classes that represent balls and the world of balls. It is the model that the view (i.e., the UI) will reflect. It is also the model that users of our application will script against. Let's begin our exploration of the Ball World application's implementation by looking at the object model.

Application Object Model

At the core of the Ball World application is an object model that includes classes for representing balls and the world of balls. Of those classes, the BallWorldViewModel class shown in Listing 10-1 is the most important to our discussion. The BallWorldViewModel class models the world of balls. Because of that, it has a private member variable called balls for containing the balls in a ball world. Each ball is an instance of the BallViewModel class. The Balls property in the BallWorldViewModel class is defined so that code outside of that class can access the balls contained in the private member variable balls. Notice that the type of the private member variable balls is ObservableCollection<BallViewModel>. The class ObservableCollection is WPF-specific and it's there to assist data binding. As you'll see later when we get to the view part of the Ball World application, the XAML code we write to define the look and feel of Ball World will use WPF data binding to bind a UI element to the Balls property of BallWorldViewModel. The way WPF data binding works is that it needs some kind of notification for updating the UI element every time the Balls property is changed. The need for notification is met by using the ObservableCollection class in Listing 10-1. Besides the Balls property and the balls member variable, other important things about the BallWorldViewModel class are the private physicsEngine member variable, the AddBall method, and the RunInitScript method. The private physicsEngine member variable is an instance of BallWorldPhysicsEngine, which we will talk about in “The Physics Engine” section later when we discuss the use of the Farseer Physics library in our application. For now, let's focus the discussion on AddBall and RunInitScript.

AddBall and RunInitScript are the methods in our application that enable application scripting. Rather than hard-coding the balls we'd like to put into a ball world, we externalize that part of the application logic into a Python script file called InitScript.py. Users can customize the Ball World application by writing their Python code in the InitScript.py file to define the balls and their colors, positions, and speed in a ball world. The RunInitScript method uses the DLR Hosting API (discussed in Chapter 6) to load the InitScript.py file. RunInitScript runs the Python code in a script scope where the variable name “world” is bound to the BallWorldViewModel instance that represents the ball world. Because the BallWorldViewModel instance that represents the ball world is in the script scope, as you will see in the next section, the Python code of InitScript.py can add balls to the ball world by calling the AddBall method on the BallWorldViewModel instance. AddBall creates an instance of BallViewModel to represent the new ball to be added to the ball world. Then it adds the new BallViewModel instance to two places. First, it adds the new BallViewModel instance to the private balls member variable so that the new ball will show up in the user interface. When this occurs, thanks to the ObservableCollection class, a property-change notification is triggered and the WPF data binding takes care of showing the new ball in the user interface automatically.

The second place where the AddBall method adds the new BallViewModel instance is the physics engine, represented by the physicsEngine member variable of the BallWorldViewModel class. This is so the physics engine knows about the new ball and can detect the collisions when the new ball collides with other balls. You'll see more code and explanations about the physics engine and collision detection in “The Physics Engine” section later in the chapter.

Listing 10-1. BallWorldViewModel.cs

public class BallWorldViewModel
{
    private BallWorldPhysicsEngine physicsEngine = new BallWorldPhysicsEngine();
    private ObservableCollection<BallViewModel> balls;
        
    public BallWorldViewModel()
    {
        balls = new ObservableCollection<BallViewModel>();
        BorderViewModel border = new BorderViewModel(
                        720f, 520f, 60, new Vector2(400f, 300f));
        physicsEngine.AddBorder(border);
        physicsEngine.Start();
        RunInitScript();
    }
    public ObservableCollection<BallViewModel> Balls
    {
        get { return balls; }
    }
    public void AddBall(Color color, float radius,
        float x, float y, float speedx, float speedy)
    {
        BallViewModel ball = new BallViewModel(color, radius,
            new Vector2D(x, y), new Vector2D(speedx, speedy));
        balls.Add(ball);
        physicsEngine.AddBall(ball);
    }

    private void RunInitScript()
    {
        ScriptRuntime scriptRuntime = ScriptRuntime.CreateFromConfiguration();
        scriptRuntime.Globals.SetVariable("world", this);
        ScriptScope scope = scriptRuntime.ExecuteFile(@"ScriptInitScript.py");
    }
}

I'll spare you the details of the BallViewModel class since it has nothing related to the DLR, nor to application scripting. If you want to see how the BallViewModel class is implemented, you can find its code in this chapter's code download.

One last thing to note about the code in Listing 10-1 is the use of the DLR Hosting API in the RunInitScript method. In RunInitScript we call the static CreateFromConfiguration method of the ScriptRuntime class. As we discussed in Chapter 6, behind the scenes, CreateFromConfiguration reads the configurations in the application's App.config file and uses them to create the script runtime. The DLR-related configurations in an App.config file determine which dynamic languages will be supported by the script runtime that CreateFromConfiguration creates. For the script runtime to support IronPython, we need to put the code in Listing 10-2 into the App.config file of the BallWorld project. Chapter 6 has a detailed explanation of the DLR-related configurations in an App.config file. If you are unfamiliar with the code in Listing 10-2, refer to Chapter 6 for more information.

Listing 10-2. App.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="microsoft.scripting"
        type="Microsoft.Scripting.Hosting.Configuration.Section, Microsoft.Scripting,
        Version=1.0.0.0, Culture=neutral" />
  </configSections>
  <microsoft.scripting>
    <languages>
      <language names="IronPython,Python,py"
                extensions=".py"
                displayName="IronPython 2.6.1"
                type="IronPython.Runtime.PythonContext,IronPython,
                        Version=2.6.10920.0, Culture=neutral" />
    </languages>
  </microsoft.scripting>
</configuration>

Application Scripting

Now let's see how to script the Ball World application by writing some Python code in the InitScript.py file, which, as you saw in the previous section, is loaded and executed by the Ball World application. The script code in InitScript.py controls the initial state of the ball world. Users can modify the Python code in InitScript.py to create a ball world that has a custom initial state without recompiling the Ball World application. Listing 10-3 shows a sample InitScript.py file. If you unzip this chapter's code download, you can find the InitScript.py file in the Chapter10BallWorldScript folder. When you open the chapter's Visual Studio solution in Visual Studio C# 2010 Express and compile the source code, the Ball World application's executable file—BallWorld.exe—will be generated in the Chapter10BallWorldinDebug folder and the InitScript.py file will be copied over to the Chapter10BallWorldinDebugScript folder. Run BallWorld.exe to launch the Ball World application. When you want to change the initial state of the Ball World application without recompiling the Ball World source code, change the InitScript.py file in the Chapter10BallWorldinDebugScript folder, not the InitScript.py file in the Chapter10BallWorldScript folder. The InitScript.py file in the Chapter10BallWorldinDebugScript folder will be refreshed with the contents you put in the InitScript.py file in the Chapter10BallWorldScript folder when you recompile the chapter's source code.

The Python code in Listing 10-3 is fairly straightforward.  First we have the line import clr because we are going to use some .NET assemblies. The line import world brings the object associated with the name “world” in the script runtime's global scope into the scope of the Python script. The script runtime has the name “world” in its global scope bound to a BallWorldViewModel instance that represents the ball world because we established that binding in the RunInitScript method of the BallWorldViewModel class. As you can see in Listing 10-3, once the variable world is in the scope of the Python script, we can define the initial state of a ball world by calling the AddBall method on the variable world. When we call the AddBall method, we specify the color, size, initial x-axis position, initial y-axis position, initial x-axis speed, and initial y-axis speed of the new ball we are creating. The coordinate system of our Ball World application has an x-axis going from left to right and a y-axis going from top to bottom. The (0, 0) origin of the coordinate system is at the very top-left corner of our application's UI. So the line world.AddBall(Colors.Blue, 50, 250, 200, -50, 50); creates a ball whose color is blue, size is 50, initial x-axis position 250, initial y-axis position 200, initial x-axis speed –50 (meaning that the ball will move to the left in the x-axis direction), and initial y-axis speed 50.

Because we use color constants like Blue, Red, and Green defined in the System.Windows.Media.Colors class to specify the color of a ball, we need to bring the Colors class into the scope of the Python script. To do so, we first use the line clr.AddReference("PresentationCore") to create a reference to the PresentationCore.dll WPF assembly because the System.Windows.Media.Colors class is packaged into that WPF assembly. Then we use the line from System.Windows.Media import (Colors) to bring the Colors class into the scope of the Python script.

Listing 10-3. A Sample InitScript.py file

import clr
import world
clr.AddReference("PresentationCore")
from System.Windows.Media import (Colors)
world.AddBall(Colors.Blue, 50, 250, 200, -50, 50);
world.AddBall(Colors.Red, 30, 200, 400, 50, 50);
world.AddBall(Colors.Black, 30, 100, 400, 20, 40);
world.AddBall(Colors.Green, 40, 250, 300, -30, 50);
world.AddBall(Colors.Purple, 35, 320, 270, 40, -40);

There are many ways to enable scripting for an application. Loading and executing script files like the Ball World application does is one way. Another way to enable scripting for the Ball World application might be to have a text box in the user interface where users can type in the script code. There could be a button by the text box that users can click to execute the script code. The Ball World application could be easily extended to support languages other than IronPython that users could write their script code in. We could open up more of the application's behaviors for users to script by exposing more methods and objects in the application's object model. Those additional methods and objects would be like the AddBall method of the BallWorldViewModel class that users can script in their Python code. No matter which way we use to enable scripting for the Ball World application, no matter which languages we support for users to write their script code in, and no matter which parts of our application we open up for scripting, the basic scripting mechanism of using the DLR Hosting API to execute dynamic language code as demonstrated in Listing 10-2 and Listing 10-3 remains the same.

Besides allowing users to change the behavior of a certain part of an application, the basic scripting mechanism demonstrated in this chapter can also be used as a plug-in infrastructure for an application. Users can use the plug-in infrastructure to extend an application by plugging in new functionalities. For example, say we are developing an image-processing application and we want to allow the image files to be saved in different file formats. Not only do we want to support the set of file formats we are aware of, we also want to allow users to plug in support for a file format that is beyond our reach. For this scenario, we could build our application in such a way that it loads a user script file containing the logic for saving the object model representing an image in our application to the user's specific file format.

If you have experience in building a plug-in infrastructure or in enabling scripting for an application using a static language like C#, you can tell how the approach demonstrated in this chapter is different. With a static language, you would need to define some classes or interfaces as the contract between your plug-in (or scripting) infrastructure and users' extensions (or scripts). When users implement their extensions to be plugged into your application, their code would need to reference the .NET assembly that contains the contract classes or interfaces in order for their code to compile. Using the DLR Hosting API to build a plug-in infrastructure or to enable application scripting, on the other hand, does not require a.NET assembly shared between you, the application creator, and people who wish to extend your application by providing plug-ins or scripts. Even though no shared .NET assembly is required to serve as the programming contract between you and people who extend your application, that doesn't mean there is no programming contract at all. In the case of the Ball World application, the contract is that the variable by the name world is accessible to script code and that the script code can invoke a method called AddBall on the variable world. The contract between the Ball World application and the InitScript.py file is checked and enforced at runtime. The contract packaged in a .NET assembly and referenced by people who extend your application is checked and enforced at compile time. A major benefit of delaying to runtime the checking and enforcement of the contract between you and users who extend your application is that users can just type in their code and have the code executed while your application is up and running. There is no need to reference contract assemblies, no need to compile code, no need to deploy the compiled code, and no need to restart the application for it to load the new version of a user's compiled code.

So far in this chapter, we have looked at how the Ball World application uses the DLR Hosting API to enable application scripting. The next section will cover the physics engine that detects ball collisions in the Ball World application. After that, we will look at the XAML code that defines the user interface of the Ball World application. The physics engine and the XAML code are not really related to application scripting. Because of that, I will go through those two components of the Ball World application only briefly and skip the details.

The Physics Engine

The Ball World application uses the Farseer Physics engine to simulate the movements of balls in a physical world. Farseer Physics is a library that provides a ton of cool features, but we're using only the collision detection feature to detect collisions between two balls or between a ball and a wall. When such a collision happens, the Farseer Physics library triggers an event that we can subscribe to and handle.

In order for the Farseer Physics engine to detect object collisions, we need to describe the walls and balls in our simulated world to the engine first. The way collision detection works in Farseer Physics version 2.1.3 is that for every object like a ball or a wall, we need to create two things in the engine: a body and the body's geometries. The body represents the object in the engine and the geometries define the object's shape. The physics engine uses objects' shapes to detect collisions among objects.

In the Ball World application, I centralized all use of the Farseer Physics engine into a class called BallWorldPhysicsEngine. Listing 10-4 shows that class's code. The three important methods of the BallWorldPhysicsEngine class are: AddBall, AddBorder, and OnCollision. The AddBall method adds a body and its geometry in the physics engine for a ball. The AddBorder method adds a body that represents the border of a ball world. The body's shape is defined by four geometries, one for each wall of the ball world. The OnCollsion method is the event handler for the OnCollision event that the Farseer Physics library triggers when two objects collide. In Listing 10-4, the OnCollision method is registered to the OnCollision event of a ball's geometry by the bolded line in the AddBall method.

The Farseer Physics engine simulates a physical world by constantly calculating the positions of bodies. The engine runs in a loop and calculates the positions of bodies in each iteration of the loop. Every time a body's position takes on a new value as determined by the physics engine's calculation, we need to be informed so we can update the Position property of the corresponding BallViewModel instance accordingly. That's why in Listing 10-4 the code in the AddBall method registers an event handler for the Updated event of the body that represents a ball in the physics engine. The physics engine will trigger the Updated event when a body's position takes on a new value, and our event handler will be called with the new position value. Inside the event handler, we update the Position property of the BallViewModel instance that corresponds to the body whose Updated event was triggered.

Listing 10-4. BallWorldPhysicsEngine.cs

public class BallWorldPhysicsEngine
{
    private PhysicsEngineLoop physicsEngineLoop;
    private PhysicsEngine physicsEngine;
    private PhysicsSimulator physicsSimulator;

    public BallWorldPhysicsEngine()
    {
        physicsEngineLoop = new PhysicsEngineLoop();
        physicsEngineLoop.IsRunningChanged += IsRunningChangedEventHandler;

        physicsEngine = new PhysicsEngine(new Vector2(0f, 0f));
        physicsSimulator = physicsEngine.PhysicsSimulator;
        physicsEngine.SetLoop(physicsEngineLoop);
    }
    public void Start()
    {
        physicsEngineLoop.Start();
    }
    public void AddBorder(BorderViewModel border)
    {
        //use the body factory to create the physics body
        Body borderBody = BodyFactory.Instance.CreateRectangleBody(
                physicsSimulator, border.Width, border.Height, 50);
        borderBody.IsStatic = true;
        borderBody.Position = border.Position;

        //left border geometry
        Vector2 geometryOffset = new Vector2(
                -(border.Width * .5f - border.BorderWidth * .5f), 0);
        CreateBorderGeom(borderBody, border.BorderWidth, border.Height, geometryOffset);

        //right border geometry
        geometryOffset = new Vector2(border.Width * .5f - border.BorderWidth * .5f, 0);
        CreateBorderGeom(borderBody, border.BorderWidth, border.Height, geometryOffset);

            
        //top border geometry
        geometryOffset = new Vector2(0, -(border.Height * .5f - border.BorderWidth * .5f));
        CreateBorderGeom(borderBody, border.Width, border.BorderWidth, geometryOffset);

        //bottom border geometry
        geometryOffset = new Vector2(0, border.Height * .5f - border.BorderWidth * .5f);
        CreateBorderGeom(borderBody, border.Width, border.BorderWidth, geometryOffset);
    }

    private void CreateBorderGeom(
        Body borderBody, float width, float height, Vector2 geometryOffset)
    {
        Geom geom = GeomFactory.Instance.CreateRectangleGeom(
            physicsSimulator, borderBody, width, height, geometryOffset, 0);
        geom.RestitutionCoefficient = 1f;
        geom.FrictionCoefficient = 0f;
        geom.CollisionGroup = 100;
    }

    public void AddBall(BallViewModel ball)
    {
        float bodyMass = ball.Radius;
        Body body = BodyFactory.Instance.CreateCircleBody(ball.Radius, bodyMass);
        body.Position = ball.Position.Vector;
        body.LinearVelocity = ball.Velocity.Vector;

        BodyModelHelper<BallViewModel> helper = new BodyModelHelper<BallViewModel>(
            ball, body,
            (UpdateEventHandler<BallViewModel>) delegate(
                        BallViewModel ball1, Vector2 position, float rotation)
            {
                ball1.Position = new Vector2D(position.X, position.Y);
                ball1.Velocity = new Vector2D(body.LinearVelocity.X, body.LinearVelocity.Y);
            });

        body.Updated += delegate { helper.Update();  };

        Geom geom = GeomFactory.Instance.CreateCircleGeom(body, ball.Radius, 60, 25);
        geom.FrictionCoefficient = 0f;
        geom.RestitutionCoefficient = 1f;
        geom.OnCollision += OnCollision;

        physicsEngine.AddBody(body);
        physicsEngine.AddGeom(geom);
    }

    private bool OnCollision(Geom geom1, Geom geom2, ContactList contactList)
    {
        float geom1Speed = geom1.Body.LinearVelocity.Length();
        float geom2Speed = geom2.Body.LinearVelocity.Length();
        if (geom1Speed > 80)
        {
            float factor = 50 / geom1Speed;
            geom1.Body.LinearVelocity.X = geom1.Body.LinearVelocity.X * factor;
            geom1.Body.LinearVelocity.Y = geom1.Body.LinearVelocity.Y * factor;
        }

        if (geom2Speed > 80)
        {
            float factor = 50 / geom2Speed;
            geom2.Body.LinearVelocity.X = geom2.Body.LinearVelocity.Y * factor;
            geom2.Body.LinearVelocity.Y = geom2.Body.LinearVelocity.Y * factor;
        }
            
        return true;
    }

    //other code omitted.
}

User Interface

The only major component of the Ball World application we haven't looked at is the part that defines the application's user interface. Because Ball World is a WPF application, its user interface is defined declaratively in XAML. For a simple application like Ball World, there is not much code we need to write to define its user interface, so I put all of the XAML code in a single file called MainView.xaml, as shown in Listing 10-5. The code mainly consists of two WPF data templates. In WPF, a data template defines the look and feel of instances of a certain class. In our example, one of the WPF data templates defines the look and feel of instances of the BallViewModel class. In other words, the data template defines what a ball looks like in our application user interface. The other data template defines the look and feel of instances of the BallWorldViewModel class. The two data templates use WPF data binding to bind UI elements to properties of the BallViewModel and BallWorldViewModel classes. For example, the data template for the BallViewModel class uses an Ellipse instance to represent a ball. The Ellipse instance's color is determined by its Fill property, which is bound by the data template to the Color property of the BallViewModel class.

Listing 10-5. MainView.xaml

<Window x:Class="BallGames.MainView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:BallGames.ViewModel"
    Title="Ball World" Width="800" Height="600">
    <Window.Resources>
        <local:ColorConverter x:Key="colorConverter" />
        
        <DataTemplate DataType="{x:Type local:BallViewModel}">
            <StackPanel>
                <Ellipse Fill="{Binding Color, Converter={StaticResource colorConverter}}"
                        Width="{Binding Diameter}" Height="{Binding Diameter}" />
            </StackPanel>
        </DataTemplate>
        
        <DataTemplate DataType="{x:Type local:BallWorldViewModel}">
            <ItemsControl ItemsSource="{Binding Path=Balls}">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <Canvas />
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemContainerStyle>
                    <Style>
                        <Setter Property="Canvas.Left"
                                Value="{Binding Path=NormalPosition.X}" />
                        <Setter Property="Canvas.Top"
                                Value="{Binding Path=NormalPosition.Y}" />
                    </Style>
                </ItemsControl.ItemContainerStyle>
            </ItemsControl>
        </DataTemplate>

    </Window.Resources>

    
    <Canvas x:Name="ballWorldCanvas">
        <ContentControl x:Name="content" />
    </Canvas>
</Window>

Toward the bottom of Listing 10-5 is a <Canvas> element that serves as the overall container for the rest of the Ball World application's UI elements. The <Canvas> element has only a <ContentControl> as the child element it contains. In the code-behind file of MainView.xaml, we set the <ContentControl> element's Content property to an instance of BallWorldViewModel as the bolded line in Listing 10-6 shows. The data template for the BallWorldViewModel class is used to render the instance of BallWorldViewModel we assign to the <ContentControl> element's Content property and we end up with the <Canvas> element containing the rest of the Ball World application's UI elements.

Listing 10-6. The Code-Behind File of MainView.xaml, MainView.xaml.cs

public partial class MainView : Window
{
    public MainView()
    {
        InitializeComponent();
        BallWorldViewModel viewModel = new BallWorldViewModel();
        content.Content = viewModel;
    }
}

Summary

This chapter looks at using the DLR and dynamic languages for application scripting—a very powerful and quite common use of the DLR. The same concepts that enable application scripting can also be used to build a plug-in infrastructure that allows users and third-party vendors to extend an application with plug-ins. To add some fun to the topic under discussion, we used a WPF application that simulates a physical world of balls to demonstrate how the DLR helps enable application scripting.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.122.244