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.
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.
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.
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.
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.
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.
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.
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.
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).
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
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.
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
).
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
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.
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
.
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()
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.
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()
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.
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.
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()
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.
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.")
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()
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).
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()
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()
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.
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()
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).
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()
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.
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()
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
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.
The DLLs and EXEs created are .NET assemblies as opposed to native code.
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")
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.
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.
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.
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.
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; } } }
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.
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.
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.
3.138.135.80