Chapter 7. Scripting via IronPython

 

"There are two ways of constructing a software design. One way is to make it so simple that there are obviously no deficiencies. And the other way is to make it so complicated that there are no obvious deficiencies."

 
 --C. A. R. Hoare

We've already discussed ways to extend application functionality in the CMS, primarily through reliance on the new Managed Extensibility Framework. This is an extremely effective method of modifying the behaviors and capabilities of the system but certainly doesn't represent the limit of our options. In this chapter, we'll look at how to implement a scripting engine in the CMS based on IronPython, as well as how we can leverage the language and interact with the CMS in a very fluid, natural way.

How Does the CMS Benefit from Scripting Capabilities?

There are a variety of reasons to incorporate scripting in the CMS: easier debugging of troublesome pages and content, rapid prototyping of ideas, and potentially allowing users to customize their pages at a code level come to mind immediately. Let's look at each of these possibilities in more detail.

Easier Debugging

As far as the developer experience of working with the CMS goes, we have already gained a significant amount of architecture flexibility by using the Managed Extensibility Framework to promote the use of plug-ins as opposed to changing the core CMS platform every time we need to make a change to the system.

A flexible architecture is great, but our real-time debugging abilities are still hindered somewhat by the fact that we (currently) have no way to interact with a page beyond the tracing and debugging facilities that the .NET Framework provides out of the box. We can capture memory dumps and analyze them in WinDbg (a topic covered in Chapter 8), but we're still a bit removed from a problematic event and the forensic analysis component of fixing problems.

By incorporating a scripting capability into the CMS, we effectively open to the door to interacting with the page in real time, which can be a tremendous asset while diagnosing problems that occur in a production environment. If we create not only the functionality to script the CMS, but the ability to persist those changes, we provide ourselves with the convenience of being able to hot-patch content without taking the CMS offline.

This ability to develop in real time and see the effect on the production system requires careful consideration and responsible development to ensure that it is used appropriately, but the benefits tie very closely into the next point, which is the efficiency provided with regard to rapid prototyping.

Rapid Prototyping

As we'll see, IronPython scripting allows us to operate on content in the CMS and make full use of the .NET Framework. From the developer standpoint, this has some pretty powerful implications.

It's possible to create an environment such that a developer can load a piece of content in the CMS, script entirely new controls and behaviors for that page, and persist those changes to the database to be used each time that content is loaded.

In practice, the act of creating complex controls becomes a bit trickier than that; truthfully, the best result is attained by taking a prototyped idea and executing it in a server-side control via the MEF plug-in system the CMS has established thus far. It does prove to be useful in that a developer can essentially work on CMS components from any machine with a functional Internet connection and web browser.

Note

I have actually done this exact routine on a production instance of the CMS. There was an issue with a plug-in that occurred only under specific conditions, and at a very late hour I was able to eliminate the problem with an IronPython script. The following morning (and after some more rest), the problem was resolved in the plug-in, and the system was immediately updated accordingly. The combination of scripting capabilities and the MEF plug-in architecture gave me the ability to respond quickly without bringing any systems offline.

An Introduction to IronPython and Its Syntax

For the purposes of the CMS, we'll be incorporating IronPython as our scripting language of choice. Although a .NET language, IronPython is very different from C#; if you're unfamiliar with the language and its syntax, we will cover the fundamentals here before adding scripting capabilities to the CMS core later in the chapter. If you're already a well-versed Pythonista, feel free to skim or skip directly ahead to the "Building Scripting Capabilities into the CMS" section.

What Is IronPython?

The shortest answer to this question is that "IronPython is an implementation of Python that is designed to run on the .NET Dynamic Language Runtime." All the traditional Python keywords and built-in functions are present, but with the additional capability of being able to make use of the .NET Framework libraries natively. As such, you could write code in IronPython that doesn't actually use any of the framework libraries at all if you so desired. If you're already well-versed in the Python language, you'll simply be gaining .NET features rather than replacing the language syntax you've become accustomed to.

The DLR is a set of services built on top of the Common Language Runtime; these services provide a dynamic type system suitable for languages such as Python and Ruby, as well as a hosting capability. As a result, code written in a traditional .NET language such as C# is able to operate on objects created in IronPython, and vice versa. This is an extremely powerful bridge between normally separate programming languages and development styles.

Note

The actual IronPython implementation is primarily written in C#; it has a very permissive Microsoft Public License, and you are free to examine the source code to IronPython at the Microsoft CodePlex site. At the time of this writing, the code can be viewed at http://ironpython.codeplex.com/sourcecontrol/changeset/view/64033?projectName=IronPython.

Unfortunately, because of the difficulties related to implementing full first-class language support in the Visual Studio IDE, IronPython still does not enjoy the benefits and development ease that C# and Visual Basic .NET do. This is by no means a reason to avoid using it; in fact, the more developers that pick up and enjoy the experience of working with Python and the .NET Framework combined, the more likely it is that support will continue to grow in the future.

Tip

Jeff Hardy, IronPython MVP and .NET programmer, expressed the difficulty of IDE support for IronPython in his post at http://jdhardy.blogspot.com/2009/12/how-hard-is-it-to-add-ironpython.html. He is the author of the Visual Studio 2010 IronPython Extensions, available at http://bitbucket.org/jdhardy/ironpython.visualstudio/. These extensions provide some very useful features to the IDE (such as syntax highlighting).

Installing IronPython

Since IronPython is not included out of the box with Visual Studio 2010, we'll have to download it from the Microsoft CodePlex site (http://www.codeplex.com/IronPython). The current stable version as of this writing is 2.6.

Once you have IronPython installed, you basically have three primary options for application development: you can write IronPython code directly into the console-based interpreter, you can save source code and execute it via the interpreter, or you can host IronPython code within another application. Although we won't have time in this chapter to cover everything related to IronPython as a language, we can begin with a brief discussion to address some key points.

For the moment, we'll use the IronPython interpreter to directly execute code for the purposes of examining the syntax of the language. You'll find this under the Programs

Installing IronPython
The IronPython interpreter (64-bit in this case)

Figure 7-1. The IronPython interpreter (64-bit in this case)

The IronPython Type System

If you've never worked with a dynamic language before, the difference in programming practices can be jarring, particularly at the outset. Listing 7-1 demonstrates the nature of IronPython's type system and how the language interprets variable assignment.

Example 7-1. IronPython Determines Variable Type Based on the Assigned Value

# note the lack of type information on the left side of the assignment
firstNumber = 5
secondNumber = "two"
thirdNumber = 42.0

If we execute those commands in the interpreter and then call type(), which is a built-in function in IronPython, on each of them, we can see that IronPython has determined each variable's type based on the inferred type of the assignment, as demonstrated in Figure 7-2. This style of programming is part of the reason that Python (and, by extension, IronPython) have become so popular among many programmers: the language is concise and lacks what is perceived to be semantic clutter. Developers with a background in C-style languages (including Java and C#) will notice that line-ending semicolons, class and method brackets, and type definitions (among other things) are all conspicuously absent from IronPython code.

Demonstrating variable typing assignments

Figure 7-2. Demonstrating variable typing assignments

It's important to note that the variable types shown in Figure 7-2 are not the .NET types as defined in the System assembly; if so, we would expect the types returned by the interpreter to be System.Int32, System.String, and System.Double, respectively. IronPython is very flexible in terms of handling types between traditional Python code and the System types; if we make use of the CLR, we can issue the variable.GetType() method to examine the .NET type information for each variable, as shown in Listing 7-2 (the results are shown in Figure 7-3).

In IronPython, we can access the .NET Framework classes via the import keyword, which serves to find a particular module and initialize it if necessary. If we needed additional namespaces from within the framework, we would import the clr module and then call the AddReference() method to access the ones required (such as System.Net or System.IO).

Tip

IronPython also allows you to handle referencing .NET namespaces via the import command. For example, you could pull in the entire System namespace with from System import *.

Example 7-2. Examining the Types of Different Variables

import clr
clr.AddReference("System")

firstNumber = 5
secondNumber = "two"
thirdNumber = 42.0

fNumType = firstNumber.GetType()
sNumType = secondNumber.GetType()
tNumType = thirdNumber.GetType()

# print the type information to the console
fNumType
sNumType
tNumType

Note

The clr.AddReference("System") call is not necessary in Listing 7-2; the type information is exposed simply by importing the clr module. I have included it simply as a demonstration of how to perform the step.

Converting to the .NET types defined in System

Figure 7-3. Converting to the .NET types defined in System

Tip

After you've assigned these types to variables, you can run conditional checks against them or examine additional properties to make program flow decisions. Given the fNumType variable in Listing 7-2, we could state Console.WriteLine("(0}", fNumType.IsValueType), which would return "True" because Int32 is a value type in the .NET Framework. This is the foundation for very useful techniques regarding the evaluation of incoming parameters and attributes when conditional branches are required; for example, if a method passes in a System.Enum, execute one branch. If a System.Double is provided, execute a different branch.

This functionality is a direct result of the fact that all value and reference types in .NET inherit either directly or indirectly from the type Object. For more information on this topic, consult the MSDN Library entry on the Object type at http://msdn.microsoft.com/en-us/library/system.object_members%28VS.100%29.aspx.

Creating Classes and Controlling Scope

Without specific scope control characters, IronPython relies on whitespace and indentation to control the scope of classes, methods, and variables. Consider the class definition in Listing 7-3; note that the scope of the method is defined by level of indentation. The ScopeTest class represents the broadest containing scope; any methods or properties within are denoted by their indentation. The instantiation at the bottom of the listing is at the same level of indentation as the class definition; therefore, it exists outside the scope of the class itself.

Figure 7-4 demonstrates the output of Listing 7-3.

Example 7-3. Demonstrating Scope Control via Whitespace and Indentation

from System import *

# the class definition is the broadest scope present
class ScopeTest:

   # this method is contained within the ScopeTest class
   def PrintMessage(self):

      # this is contained within the PrintMessage() method
      Console.WriteLine("This is a demonstration of IronPython scope control.")


# these are at the class level and will instantiate it, then call PrintMessage()
scopeTest = ScopeTest()
scopeTest.PrintMessage()

Note

What's the deal with the variable self being passed to the PrintMessage() method given that it's apparently not used? Hold that thought, because we will discuss the importance of self in a few pages.

The output of the scope demonstration

Figure 7-4. The output of the scope demonstration

Adding another class at the same indentation level as the other class definition creates a separate scope for its members, as demonstrated in Listing 7-4. The output of this code is displayed in Figure 7-5.

Example 7-4. Adding a Second Class Demonstrates the Scope of Each

from System import *

# the class definition is the broadest scope present
class ScopeTest:

   # this method is contained within the ScopeTest class
   def PrintMessage(self):

      # this is contained within the PrintMessage() method
      Console.WriteLine("This is a demonstration of IronPython scope control.")

# a second class definition
class NewScope:

   # this method is contained within the NewScope class
   def PrintMessage(self):

      # this is contained within the PrintMessage() method
      Console.WriteLine("This is a second class with its own scope.")

# these are at the class level and will instantiate it, then call PrintMessage()
scopeTest = ScopeTest()
scopeTest.PrintMessage()

scopeTest2 = NewScope()
scopeTest2.PrintMessage()
The new class has a separate scope based on indentation.

Figure 7-5. The new class has a separate scope based on indentation.

Note

As this code demonstrates, IronPython instantiates classes in the form instance = className(parameters). After you have assigned a class to an instance, you can use the standard dot notation familiar to C# developers to call methods in the form instance.Method(parameters) or assign return values in the form variableName = instance.Method(parameters).

We can display the instance information about each object by simply passing the object itself as output to the console, as demonstrated in Listing 7-5. Note the output that is displayed in Figure 7-6; scopeTest is an instance of the ScopeTest class, while scopeTest2 is an instance of the NewScope class.

Example 7-5. Displaying the Instance Information About the Objects We Created

# displaying the types of each instance
Console.WriteLine("scopeTest instance is {0}", scopeTest)
Console.WriteLine("scopeTest2 instance is {0}", scopeTest2)
The instances are displayed, demonstrating the effects of scope control

Figure 7-6. The instances are displayed, demonstrating the effects of scope control

Constructors as Magic Methods

IronPython supports constructors via the use of what are affectionately termed magic methods. Named for their ability to automatically provide object functionality simply by being called, magic methods fulfill a variety of roles; some are related to the creation of objects, some to iteration, some to comparison, and so on. Covering every magic method in IronPython is outside the scope of this chapter, but we will touch on important ones as necessary.

The first (and certainly critical) example for many developers is that of the class constructor. In C#, the constructor for a class is typically the class name with zero or more parameters supplied depending on the requirements of the class. In IronPython, constructors are magic methods, and as such are in the form __init__(self).

All IronPython's magic methods are prepended and appended with two underscore (_) characters each. Note that the constructor is a method and requires the passing of the self (a requirement we will discuss next). Listing 7-6 demonstrates creating a constructor using the __init__ magic method, and Figure 7-7 shows the output of that code.

Example 7-6. Creating a Constructor for an IronPython Class

class Test:

   def __init__(self):
      print "Test class instanced."

   def Method(self):
      print "Method called."

instance = Test()
instance.Method()
Using the constructor, which is a magic method

Figure 7-7. Using the constructor, which is a magic method

self

In the examples shown thus far, each method signature takes at least one variable as a parameter: self. In IronPython, all methods have to take self as the first argument in their signature. If you define a method without providing the self variable, IronPython will throw an error indicating that the method takes no arguments but 1 was provided (self is passed implicitly within the method call).

Listing 7-7 shows an example of a method that does not accept self, and Figure 7-8 demonstrates the error that the interpreter will return in such cases.

Example 7-7. Attempting to Create a Method That Does Not Accept self

class Test:

   def Method():
      return "This method was called successfully."

instance = Test()
instance.Method()

Note

In IronPython, self is analogous to the C# keyword this. The difference is that the definition of the self variable is a requirement in IronPython, whereas in C# this is implicit.

Without accepting self, the code is unable to execute because the method signature doesn't match expectations.

Figure 7-8. Without accepting self, the code is unable to execute because the method signature doesn't match expectations.

Although self is expected to appear first in the method signature, it certainly doesn't have to be the only parameter supplied; Listing 7-8 demonstrates a comma-separated list of parameters following self in the method signature, and Figure 7-9 demonstrates the successful passing of parameters as well as the implicit nature of the self variable.

Example 7-8. self Is Expected to Be the First, but Not Necessarily Only Parameter

class Test:

   def Method(self, input):
      print "You entered", input

instance = Test()
instance.Method("some text.")
Method signatures work the same as C# but with one reserved variable position.

Figure 7-9. Method signatures work the same as C# but with one reserved variable position.

The self variable is also important with respect to accessing properties and methods within a particular class. For instance, if we modify the Method() section to call a new method named DoWork(), we must use the self variable to successfully reference it. Listing 7-9 uses the self variable to call the DoWork() method of the Test class (the results are shown in Figure 7-10).

Example 7-9. We Can Access DoWork() by Relying on self

class Test:

   def Method(self):
      self.DoWork()

   def DoWork(self):
      print "Method() called DoWork()."

instance = Test()
instance.Method()
Using self to access members within the class

Figure 7-10. Using self to access members within the class

Finally, although the convention to use self is expected and typical among IronPython developers, you're not bound to that specific word; it is a variable that holds a reference to the instance, and the name itself holds no particular meaning. As such, you can change it to something else if desired. Listing 7-10 shows methods that use foo instead of self to achieve the same effect, at the expense of breaking convention (the results are shown in Figure 7-11).

Example 7-10. Although Not Necessarily Recommended, You Can Use a Different Variable Name Than self

class Test:

   def Method(foo, input):
      foo.DoWork(input)

   def DoWork(bar, input):
      print "You entered", input

instance = Test()
instance.Method("some text.")
self as a variable name has no special meaning and can be changed if desired.

Figure 7-11. self as a variable name has no special meaning and can be changed if desired.

Warning

IronPython demonstrably won't complain if you change self to some other name, but unless you have a compelling reason to do so, changing it is probably not worth the potential loss of clarity and convention.

Exception Handling

IronPython supports a try/catch approach to exception handling that will be very familiar to .NET developers. In place of the catch keyword, IronPython uses except. The try/except block is used to help manage errors that may arise during the runtime of an application.

If we attempt to divide by 0 in IronPython, we get a ZeroDivisionError exception informing us that a critical error has occurred, and the program immediately ceases execution. Wrapping this operation in a try/except block doesn't prevent the error from occurring, but it does provide an opportunity to respond without aborting the remainder of the application, as shown in Listing 7-11. You can see the results in Figure 7-12.

Example 7-11. Wrapping a Sensitive Operation in a try/except Block

class Test:

   def CauseError(self):
      try:
         print 0 / 0
      except:
         print "Exception caught."
instance = Test()
instance.CauseError()
The try/except block allows response to error conditions without terminating execution.

Figure 7-12. The try/except block allows response to error conditions without terminating execution.

The except block in Listing 7-11 is functional but very broad; it will catch any exception thrown by the code in the try block. The limitation with this approach is that we're not able to provide context-sensitive branches of logic based on the type of exception that was actually thrown. For example, if a ZeroDivisionError exception is thrown, we may want a different response sent to the user than other exceptions.

As in the traditional .NET languages, IronPython allows the filtering of exceptions where the most specific matching exception type hit first is used. Therefore, it is appropriate to structure the except block such that the most likely (and specific) exceptions appear first, moving toward the most generic. In Listing 7-12, the ZeroDivisionError exception appears first; if we had placed it after the generic except block, it would never be hit. Running this code results in the output shown in Figure 7-13.

Example 7-12. Handling Specific Exception with Except Blocks

class Test:

   def CauseError(self):
      try:
         print 0 / 0
      except ZeroDivisionError:
         print "Unable to divide a number by zero."
      except:
         print "General exception caught."
instance = Test()
instance.CauseError()
The general except block is never triggered because a more specific exception was caught.

Figure 7-13. The general except block is never triggered because a more specific exception was caught.

IronPython also supports a finally block that will execute regardless of what exceptions are triggered, as shown in Listing 7-13. This is the appropriate place for (among other things) freeing resources and closing connections as you are guaranteed that code within the block will run. The results of running this code are shown in Figure 7-14.

Note

I mention these two operations in particular because they are historically known to be application killers if not managed properly.

Example 7-13. Handling Specific Exception with Except Blocks

class Test:

   def CauseError(self):
      try:
         print 0 / 0
      except ZeroDivisionError:
         print "Unable to divide a number by zero."
      except:
print "General exception caught."
      finally:
         print "Executed regardless of exceptions."

instance = Test()
instance.CauseError()
Code within a finally block is executed regardless of whether an exception is thrown.

Figure 7-14. Code within a finally block is executed regardless of whether an exception is thrown.

Conditional Logic, Iterators, and Collections

In IronPython, a custom object supports iteration if it supplies a magic method called __iter__(self) that allows the return of incremental items within a set of elements. Many built-in types already provide this functionality; the code in Listing 7-14 shows a set of string objects iterated to display them in storage order (the results are shown in Figure 7-15).

Tip

The .NET Framework provides an interface, IEnumerable, that can be implemented to provide iteration capabilities to custom objects.

Example 7-14. Iterating over a Tuple of String Objects

class Test:

   def IterateElements(self):
      elements = ("hydrogen", "helium", "lithium", "beryllium", "boron")
for element in elements:
         print element

instance = Test()
instance.IterateElements()

Tip

The elements variable actually contains a tuple; in IronPython, tuples are immutable objects. They behave similarly to lists, which are in the form variableName = [element, ..., element]. A list, by contrast, is mutable. Specific examples within the language are strings and dictionaries; strings are tuples of characters and therefore immutable, while dictionaries are mutable.

Iterating over a tuple of elements

Figure 7-15. Iterating over a tuple of elements

The language also supports conditional logic in the form of the if/then/else construct (although the IronPython form is if conditions: ... else:), as shown in Listing 7-15. Running the code results in output shown in Figure 7-16.

Example 7-15. Iterating Over a Tuple of String Objects with a Conditional Check for a Particular One

class Test:

   def IterateElements(self):
      elements = ("hydrogen", "helium", "lithium", "beryllium", "boron")
      for element in elements:
         if element == "lithium":
            print "Found lithium during iteration."
else:
            print "Element", element, "is not a match for lithium."

instance = Test()
instance.IterateElements()
A conditional check for a particular string

Figure 7-16. A conditional check for a particular string

Accessors and Mutators

In older versions of IronPython, properties (accessors and mutators) were implemented in the traditional Python way, which required overriding two magic methods: __getattr__ and __setattr__. There are technical issues with this route that include (but aren't limited to) performance and ease of implementation. Luckily, as the language has matured, properties have as well, resulting in a more natural syntax that should be somewhat familiar to C# developers (although a bit more verbose).

IronPython requires that we define a bit of information for properties. Specifically, we must define both the accessor and mutator methods, as well as execute assignment of an attribute that maps the methods as necessary. For example, consider the class in Listing 7-16; getName(self) and setName(self, name) are the accessor and mutator, respectively. They each operate on a variable called name, which is declared to be a property on the 6th line; it is at this point that the methods are properly mapped to the variable. The results are shown in Figure 7-17.

Example 7-16. Defining a Set of Properties for the Test Class

class Test:

   def getName(self):
      return self.__name
   def setName(self, name):
      self.__name = name
   name = property(getName, setName)

instance = Test()
instance.name = "Alan" # mutator; calls the setName() method
print instance.name # accessor; calls the getName() method
Using properties in an IronPython class

Figure 7-17. Using properties in an IronPython class

Assembly Compilation

Although we will not be focusing on distribution of code to third parties, it remains a possibility that may become a reality as you work with IronPython further. In many cases, it's neither a good idea nor permissible by your organization to distribute your source code directly to the outside world. IronPython supports compilation to either dynamic link libraries (DLLs) or to executables.

Note

The DLLs and EXEs created are .NET assemblies as opposed to native code.

Compiling IronPython Code to a DLL

It is by no means a requirement to distribute your IronPython code directly as text-based script files; the IronPython interpreter provides a way for us to compile our source code to DLLs. This functionality is exposed via the clr.CompileModules() method; with it we can specify the files we want to include as well as the desired output file.

For the sake of organization, let's create a folder to store our scripts. We'll create this location separate from the CMS, in no small part to ensure a physical separation between the core project and the extensibility elements that will be applied to it. For these examples, I am assuming you have a folder called IronPython in the root of the C drive. Once you have a location, add the code in Listing 7-17 to a file within it called test.py. We will not be compiling any IronPython code in our CMS; instead, we'll simply look at how to compile from within the interpreter.

Example 7-17. A Simple IronPython File That Will Be Compiled to a DLL

from System import *

def Test(self):
   Console.WriteLine("This is compiled IronPython code")

Now open an instance of the IronPython interpreter. You will first need to issue the import clr command to be able to use the CompileModules() method. This method accepts the desired output file, followed by parameters indicating which files to include in generating the assembly. To generate a file called test.dll, we need to issue the following command to the interpreter (see Figure 7-18 for an example of the interpreter session):

clr.CompileModules("C:\IronPython\test.dll", "C:\IronPython\test.py")

Tip

For the purposes of demonstration, I used the fully qualified paths to each object in question. If you plan to do any real work along this line, modify your environment path as a time-saver.

Compiling IronPython code to a DLL

Figure 7-18. Compiling IronPython code to a DLL

At this point, we have a complete .NET assembly in the form of a DLL. If you're interested in exploring the IL that gets generated to accomplish this, run ILDASM on the test.dll file, and explore the various nodes, which are shown in Figure 7-19. It's worth noting how much IL code is required to accomplish the functionality expected with an IronPython application.

Using ILDASM to explore the IL generated by compiling IronPython code

Figure 7-19. Using ILDASM to explore the IL generated by compiling IronPython code

Compiling IronPython Code to an Executable

You can compile IronPython code to a Windows executable file if desired. We won't be spending too much time on this subject because it's not something we'll use in the CMS, but it may be something you want to explore in the future.

Assuming that we have a file called test.py that defines the necessary structure of a Windows application (including an entry point and so on), we can compile it to an executable by using the pyc.py script located in the Tools/Scripts folder within the IronPython installation, as shown in Figure 7-20. The pyc.py script accepts the files involved in the compilation and allows you to define the entry point and target type.

Compiling an IronPython executable is slightly more involved.

Figure 7-20. Compiling an IronPython executable is slightly more involved.

Building Scripting Capabilities into the CMS

IronPython makes an excellent option as a scripting language for the CMS. Besides being a .NET implementation of the popular (and stable) Python language, it possesses a clear, concise nature and style that lends itself well to rapid development tasks. Languages such as IronRuby exist as alternatives, but IronPython is arguably the most mature among the lot.

Although we've covered how to compile IronPython code to an assembly, the desired functionality within the CMS is that we want to be able to do limited scripting on individual pieces of content, or at the embeddable level. This may involve IronPython script files saved to the file system (which would be a task performed by system administrators and developers), or it may be accomplished through inline scripts created by end users (to whom we will provide a limited API and access to the Page object itself).

To achieve this, we want to be able to execute IronPython code on the fly from within C#, which hinges directly on the ability to host the IronPython engine in your application. This engine is exposed via the ScriptEngine class and is directly paired with a ScriptScope object, which defines the scope of the variables we may want to pass back and forth to our code.

The code in Listing 7-18 is the implementation of the Scripting class in the CMS. It resides in the Business library within a folder called Scripting. The constructor sets up instances of the engine and the variable scope, and it provides methods for executing inline scripts as well as scripts that have been persisted.

Example 7-18. A Scripting Class to Handle Execution of IronPython Scripts

using System;
using System.Configuration;
using Microsoft.Scripting;
using Microsoft.Scripting.Hosting;
using IronPython.Hosting;

namespace Business.Scripting
{
    public class Scripting
    {
        private ScriptEngine _engine;
private ScriptScope _scope;

        /// <summary>
        /// Ctor; creates an instance of the ScriptEngine and ScriptScope automatically
        /// </summary>
        public Scripting()
        {
            _engine = Python.CreateEngine();
            _scope = _engine.CreateScope();
        }

        /// <summary>
        /// Executes an IronPython script from the web.config-dictated location.
        /// </summary>
        /// <param name="fileName">The name of the script file.</param>
        /// <param name="className">The class to instantiate.</param>
        /// <param name="methodName">The method to execute.</param>
        /// <param name="parameters">
        /// Any parameters the method needs to execute successfully.
        /// </param>
        /// <returns>
        /// Dynamic; dictated by the returned information (if any) of the script.
        /// </returns>
        public dynamic ExecuteFile(string fileName, string className, string methodName,
                                   [ParamDictionary] params dynamic[] parameters)
        {
            try
            {
                _engine.ExecuteFile(ConfigurationManager.AppSettings["ScriptsFolder"] + @""
                  + fileName, _scope);
                var classObj = _scope.GetVariable(className);
                var classInstance = _engine.Operations.Call(classObj);
                var classMethod = _engine.Operations.GetMember(classInstance, methodName);
                dynamic results;
                if (parameters != null)
                {
                    results = _engine.Operations.Call(classMethod, parameters);
                }
                else
                {
                    results = _engine.Operations.Call(classMethod);
                }
                return results;
            }
            catch
            {
                return null;
            }
        }

        /// <summary>
        /// Executes an arbitrary-length IronPython script.
        /// </summary>
        /// <param name="script">The IronPython code itself.</param>
/// <returns>
        /// Dynamic; dictated by the returned information (if any) of the script.
        /// </returns>
        public dynamic Execute(string script)
        {
            try
            {
                return _engine.Execute(script, _scope);
            }
            catch
            {
                return null;
            }
        }
    }
}

The Execute() method accepts a script as a string object and executes it without accepting any additional parameters. The implication here is that the script is a self-contained unit of IronPython code; for example, you may include a script that performs some specific computation and returns a result to the calling class. In contrast, the ExecuteFile() method runs scripts from the file system.

There are some interesting things to note here. First, the return type for each of the script execution methods is dynamic, which (as we've discussed) is new in .NET 4. Remember that the dynamic type is effectively a placeholder for a static type that will be resolved at runtime. The benefit to using it in this fashion is that it provides extremely robust as well as brief code. You could choose to base this class around generics, as in Scripting<string> script = new Scripting<string>(). In general, although this approach is functional, the result is more code for less return compared to using the new dynamic type.

The code for handling an inline script is very different than for a specific file. In the ExecuteFile() method, we are concerned with scope, class names, methods, and parameters. The ScriptScope will be valid for the lifetime of the page and then expire; if the class were defined as static, our changes to data within the scope would impact every other user currently browsing the site. Limiting the scope to the lifetime of the page enforces some logistical walls between scripts.

Also worth noting is the expectation in this class that there will be a setting in the CMS's web.config file that defines the location of the file system-based scripts; this attribute is expected to be named ScriptsFolder. This becomes relevant when we want to open scripts directly from a folder on a drive and run it via the ExecuteFile() method.

Handling Script Files Between Tiers

As multiple tiers in the CMS will need to perform tasks based on IronPython scripts, it makes sense to create an entity called ScriptedFile in the CommonLibrary project that will define the necessary properties that go into said scripts, as shown in Listing 7-19. Note that the constructor accepts a dynamic array of parameters; we will very shortly look at the effects of passing in a reference to the current ASP.NET Page object.

Example 7-19. An Entity in the CommonLibrary Project That Will Reference a Particular Script File

using System;

namespace CommonLibrary.Entities
{
public class ScriptedFile
    {
        public string fileName { get; set; }
        public string className { get; set; }
        public string methodName { get; set; }
        public dynamic[] parameters { get; private set; }

        public ScriptedFile(dynamic[] parameters)
        {
            this.parameters = parameters;
        }
    }
}

Calling Scripts for a CMS Page

The code in Listing 7-20 is fairly long and represents the code for content.aspx.cs; recall that all public CMS pages are delivered through this page. The new sections related to loading and operating on scripts have been highlight; the discussion of what has been added (and why) will follow the code itself. Note that any method in the content.aspx.cs page that did not require modification for handling IronPython scripts has been excluded from Listing 7-20.

Example 7-20. An Entity in the CommonLibrary Project That Will Reference a Particular Script File

using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
using CommonLibrary.Entities;
using Business;
using Business.Scripting;

public partial class content : System.Web.UI.Page
{
    private Scripting _scriptEngine;
    private string _script;
    private List<ScriptedFile> _scriptFiles;

    protected void Page_Load(object sender, EventArgs e)
    {
        // initialize IronPython scripts and lists of files
        _script = String.Empty;
_scriptFiles = new List<ScriptedFile>();

        if (Request.QueryString["id"] != null)
        {
            Guid id = new Guid(Request.QueryString["id"]);
            form1.Action = HttpContext.Current.Request.RawUrl.ToString().ToLower();
            LoadTemplate(id);
            LoadContent(id);
            ExecuteScripts();

            // modify page based on available plug-ins
            var business = new Business.Plugins();
            var embeddables = business.ExecuteEmbeddablePlugins(this);
            foreach (var embed in embeddables)
            {
                this.Controls.Add(embed);
            }
        }
        else
        {
            return;
        }
    }

    /// <summary>
    /// Loads the template for a page.
    /// </summary>
    /// <param name="id">the ID of the page</param>
    private void LoadTemplate(Guid id)
    {
        List<TemplateRow> template = Business.Templates.LoadTemplate(id);
        foreach (var t in template)
        {
            LoadBuckets(t.bucketControl, t.embeddableControl);
            foreach (var script in t.scriptFiles)
            {
                _scriptFiles.Add(script);
            }
        }
    }

    /// <summary>
    /// Loads the content for a page.
    /// </summary>
    /// <param name="id">the ID of the page</param>
    private void LoadContent(Guid id)
    {
        List<ContentRow> content = Business.Content.LoadContent(id);
        foreach (var c in content)
        {
            LoadBuckets(c.bucketControl, c.embeddableControl);
            foreach (var script in c.scriptFiles)
            {
                _scriptFiles.Add(script);
}
        }
    }

    /// <summary>
    /// Loads an IronPython intepreter and executes IP scripts for this content.
    /// </summary>
    private void ExecuteScripts()
    {
        _scriptEngine = new Scripting();
        dynamic[] parameters = new dynamic[1];
        parameters[0] = Page;

        // iterate over any .py scripts attached to this page
        foreach (var script in _scriptFiles)
        {
            _scriptEngine.ExecuteFile(script.fileName, script.className, script.methodName,
                                      script.parameters);
        }

        // execute any inline script
        _scriptEngine.Execute(_script);
    }

Most of the changes should be fairly straightforward. We have defined variables to hold inline script information as well as a list of script files and their relevant information to be called. Notice that the ExecuteScripts() method is automatically configured to pass a reference to the current Page object down into any of the scripts within the system. This is a design choice that makes a great deal of sense; by passing a reference to the Page object to the script, the script now has full control over the final page and is capable of making significant modifications to it.

A Simple Scripting Example

Listing 7-21 demonstrates how simple (yet effective) the IronPython scripting experience can be; we've effectively been handed the reins and can operate on the page just as we would traditionally in C#. Listing 7-21 identifies specific buckets on the page using the FindControl() method. Recall from Chapter 2 that each bucket contains a PlaceHolder control called embeddables, which exists to house the specific embeddable subcontrols that a page might need. The script identifies this PlaceHolder within the bucket and loads a specific user control into it.

Example 7-21. Using the Page Reference to Load Controls into the Final Hierarchy

class Test:
   def Method(self, page):
      header = page.FindControl("header").FindControl("embeddables")
      header.Controls.Add(page.LoadControl("~/core/embeddables/tags.ascx"))

      subnav = page.FindControl("subnav").FindControl("embeddables")
      subnav.Controls.Add(page.LoadControl("~/core/embeddables/article.ascx"))

For the purposes of testing, we can modify the ExecuteScripts() method in the content.aspx.cs page with a hard-coded set of values. Assuming that the code in Listing 7-21 exists in the PluginsScripts folder as defined in the CMS's web.config file, the modification in Listing 7-22 will automatically execute it during the normal page life cycle.

Example 7-22. Hard-coding a Script for Testing Purposes

/// <summary>
/// Loads an IronPython intepreter and executes IP scripts for this content.
/// </summary>
private void ExecuteScripts()
{
    _scriptEngine = new Scripting();
    dynamic[] parameters = new dynamic[1];
    parameters[0] = Page;

    _scriptFiles.Add(new ScriptedFile(parameters) { className = "Test", fileName = "test.py", methodName = "Method"});

    // iterate over any .py scripts attached to this page
    foreach (var script in _scriptFiles)
    {
        _scriptEngine.ExecuteFile(script.fileName, script.className, script.methodName, script.parameters);
    }

    // execute any inline script
    _scriptEngine.Execute(_script);
}

Finally, we can see the results of the script's operation in Figure 7-21. The script has effectively doubled up the controls in each bucket as expected.

The IronPython script has modified the page control hierarchy.

Figure 7-21. The IronPython script has modified the page control hierarchy.

Summary

Beginning with a tour of the IronPython language and syntax, we covered some of the key areas and features developers use regularly. Although an exhaustive discussion of the language as a whole is beyond the scope of a single chapter, the details covered are sufficient to begin scripting in the CMS in a meaningful and useful way. We discussed and implemented the modifications to the content.aspx.cs page such that scripts would be loaded and executed when the page is delivered to the user, and we ended the chapter by building a simple script that loads user controls into specific locations within a particular page.

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

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