The Tridion GUI is powered by JavaScript while the backend is powered by .Net and sending a (nice) message from .Net to the GUI has been very challenging – until now. Using SignalR, an open-source product from Microsoft, it is possible to show or ‘echo’ the message from the .Net Event System, Templates or Customer Resolvers and show them in the GUI. Check out the tutorial that Will Price and I wrote published on SDL Tridion World to find out how to implement this yourself. Special thanks to SDL’s Bart Koopman for helping get the article online and embracing the GitHub Gist embed for source code.

Tridion 2013 works with both Visio 2010 and 2013, and removes support for older versions of Visio.  Today I tried to create a new Workflow using Visio 2013 and I would like to share an important tip that might save you some time.

The Tridion Visio Stencils are located in the ‘ADD-INS’ menu item.

However, attempting to create a new Workflow will be unsuccessful in your default Visio / Tridion workflow install.  You will see the following error message:

Visio blocks these ADD-INS for our own security (was there ever a problem with rampant Visio plugins??)

Solution:

1.  Go to File, Options
2.  Trust Center Settings


3.  File Block Settings 

4. UnCheck all checkboxes.  Yes, Uncheck them.   I know it seems unintuitive, but we are in the ‘Block’ settings and we do not want Visio to block us from opening or saving these item types.

5.  Select OK and then go back to your new Visio Workflow diagram.  It should allow you to create a new Workflow diagram now.

The icons all look almost the same, and the tooltips don’t give us any hints.  For me this was a struggle to figure out which save button to press.  Hopefully this will be fixed in a future SP.

Tridion C# TBB Upload Tip

June 10th, 2013 | Posted by Robert Curlette in .NET | Tridion - (0 Comments)

When saving a C# TBB I recently had the error message ‘Error 1 Invalid URI: The format of the URI could not be determined.’?

My first thought was to check the URI in the AssemblyInfo.cs file – was it the correct folder URI?  Did I have a typo?  Did the folder exist?  But, all was fine.

Then I recalled seeing this error before and it is related to the config.xml file that we create using TcmUploadAssembly.exe to upload the TBB.  In this file we must specify the URL of the server, which is, of course, also a URI.

The wrong value was:  <targetURL>localhost</targetURL>

Correct:  <targetURL>http://localhost</targetURL>

So, just in case you ever hit this error I hope this little tip can save you some time.

Sometimes, especially for System Administration or Developer Extensions, we don’t want to have the Context Menu GUI Extension visible to the end user.

We can use the IsAvailable option in the GUI Extension Command.js file and set it to false if the user is in a not allowed group. The following code can be used for hiding a GUI Extension context menu item.

Extensions.SetPermissions.prototype.isAvailable = function SetPermissions$isAvailable(selection, pipeline) {
    var showOption = true;
	var groupsWithNoAccess = ["Author","Chief Editor"];

	// Get the groups the user belongs to
	var groups = Tridion.UI.UserSettings.getJsonUserSettings(true).User.Data.GroupMemberships;
	if(groups["@title"] == undefined)  // 1 group membership
	{
		if(groups.Group["@title"] == groupsWithNoAccess[0])
			showOption = false;
	}
	else
	{
		// if the user belongs to a group with no access, hide the option
		for (var i = 0; i < groups.length; i++) {
		    var userIsInGroup = groups[i]["@title"];
		    length = groupsWithNoAccess.length;
		    while(length--) {
		         if (userIsInGroup.indexOf(groupsWithNoAccess[length])!=-1) {
			      showOption = false;
		              return showOption;
			 }
		     }
		}
	}
	return showOption;
};

 

Show Context Menu Item for Authorized Groups
And, if we want th do the inverse, and show the Context Menu item for only authorized groups we would use the following:


Extensions.SetPermissions.prototype.isAvailable = function SetPermissions$isAvailable(selection, pipeline) {
        var showOption = false;
	
	var groupsWithAccess = ["Administrator"];  // Specify all access groups here
	
	var groups = Tridion.UI.UserSettings.getJsonUserSettings(true).User.Data.GroupMemberships;
	if(groups["@title"] == undefined)  // 1 group membership
	{
		if(groups.Group["@title"] == groupsWithAccess[0])
			showOption = true;
	}
	else
	{
		// many group memberships
		for (var i = 0; i < groups.length; i++) {
			var userIsInGroup = groups[i]["@title"];		
			length = groupsWithAccess.length;
			while(length--) {
			   if (userIsInGroup.indexOf(groupsWithAccess[length])!=-1) {
				   showOption = true;
				   return showOption;
			   }
			}
		}
	}
	
	return showOption;
};

In my previous article I described how to use a Console application to check-in items using the Core Service. In this article I will describe how to setup and use the log4net framework to output content to the Console window and also to a log file.

1. Get the log4net DLLs using NuGet. Right-click on the project name and select ‘Manage Nuget Packages’. If you do not have this option then download NuGet and then try again.

2. Update the app.config file with the log4net configuration. NuGet packages can modify our config files, but unfortunately the log4net NuGet package does not.

This configuration does the following:
– Specifies a Console Logger to automatically write all messages to the console
– Uses a Date rolling file appender

<configuration>
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
</configSections>
<log4net>
<appender name="Console" type="log4net.Appender.ConsoleAppender">
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date %-5level: %message%newline" />
</layout>
</appender>
<appender name="LogFileAppender" type="log4net.Appender.RollingFileAppender">
<param name="File" value="CheckinItems.log"/>
<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
<appendToFile value="true" />
<rollingStyle value="Date" />
<maxSizeRollBackups value="7" />
<maximumFileSize value="10MB" />
<staticLogFileName value="true" />
<layout type="log4net.Layout.PatternLayout">
<param name="ConversionPattern" value="%-5level %date{yyyy-MM-dd HH:mm:ss} - %m%n"/>
</layout>
</appender>
<root>
<level value="ALL" />
<appender-ref ref="LogFileAppender" />
<appender-ref ref="Console" />
</root>
</log4net>
</configuration>

3. Add this code to your Main method to startup log4net

XmlConfigurator.Configure();

4. Instantiate an instance of a logger

private readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

5. Write a log message

log.Info("log something");

What happens if you have hundreds, or thousands of items you want to check-in? One solution is to check-in the items using the GUI, My Tasks, All Checked out items option. However, there is another way and that is to run a script to check-in the items automatically. This is also helpful if you want to check-in all items before running another script.

Running scripts in Tridion is a common activity on large implementations. There’s always some data manipulation we want to do in bulk and save the Editors and Authors hours of time. Sometimes, however, during these bulk operations we have a failure, and sometimes items remain checked out.

In this article I will describe how to create a Core Service Console Application to check-in Tridion items. The following code uses the Tridion Core Service and can run from any Tridion instance.

Step 1: Creating the Core Service App and References

1. Create a new Console Application.

2. Reference the Tridion Core Service DLL

3. Create an App.config file

Copy the Core Service config from C:\Program Files (x86)\Tridion\bin\client\Tridion.ContentManager.CoreService.Client.dll.config

4. Reference 2 Microsoft DLLs:

  • System.Runtime.Serialization
  • System.ServiceModel

5. Add the using statement for the Tridion Core Service.

using Tridion.ContentManager.CoreService.Client;

Summary:
Creating the core Service app is relatively simple and straight-forward. The good news is we now have everything we need to get started writing some code.

Step 2: Using the Core Service to find Checked-out items

1. Set the binding. The Tridion Core Service comes with 2 binding options. It depends on your client (.Net, Java, etc) and also the ports avaialble (netTCP requires port 2660). For this example I will ue NetTCP since it is the fastest binding and most often used.

The binding is in the App.config file.

<endpoint name="netTcp_2011"

// use the endpoint name in our C# code
class Program
    {
        private static string binding = "netTcp_2011";
		//....
	}

2. Create an instance of the Core Service client and get all checked out items. * The magic here is in which filter to use. Big thanks to Andrey (aka Mr. P) for his help. how-do-i-get-a-list-of-checked-out-items-with-the-core-service

I always wrap the code in a using statement to displose of resources. The SystemWideListFilter is a special one that has magic powers to get items not conatined in any Blueprint Publication.

public static XElement FindCheckedOutItems()
{
	using (SessionAwareCoreServiceClient client = new SessionAwareCoreServiceClient(binding))
	{
		RepositoryLocalObjectsFilterData filter = new RepositoryLocalObjectsFilterData();
		XElement checkedOutItemsXml = client.GetSystemWideListXml(filter);
		return checkedOutItemsXml;
	}           
}

Step 3: Using the Core Service to Check-in items

1. Check-in the items with the Core Service

private static void CheckinItems(XElement items)
{
	using (SessionAwareCoreServiceClient client = new SessionAwareCoreServiceClient(binding))
	{
		foreach (XElement tridionItem in items.Nodes())
		{
			if (tridionItem.Attribute("Type").Value == "16" || tridionItem.Attribute("Type").Value == "64")
			{
				client.CheckIn(tridionItem.Attribute("ID").Value, new ReadOptions());
			}
		}
	}
}

That’s it. Here’s the final solution code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Tridion.ContentManager.CoreService.Client;
using System.Xml.Linq;

namespace CheckinItems
{
    class Program
    {
        private static string binding = "netTcp_2011";
        static void Main(string[] args)
        {
            CheckinItems(FindCheckedOutItems());
        }

        public static XElement FindCheckedOutItems()
        {
            using (SessionAwareCoreServiceClient client = new SessionAwareCoreServiceClient(binding))
            {
                RepositoryLocalObjectsFilterData filter = new RepositoryLocalObjectsFilterData();
                XElement checkedOutItemsXml = client.GetSystemWideListXml(filter);
                return checkedOutItemsXml;
            }           
        }

        private static void CheckinItems(XElement items)
        {
            using (SessionAwareCoreServiceClient client = new SessionAwareCoreServiceClient(binding))
            {
                foreach (XElement tridionItem in items.Nodes())
                {
                    if (tridionItem.Attribute("Type").Value == "16" || tridionItem.Attribute("Type").Value == "64")
                    {
                        client.CheckIn(tridionItem.Attribute("ID").Value, new ReadOptions());
                    }
                }
            }
        }
    }
}

Summary

The Core Service is a powerful tool in our Tridion toolbelt. With the right filters we can work magic, retrieving lists of items never thought possible. A big thanks for the help from the Tridion community for sharing information!

MVP Award, 2013 and Learning

January 22nd, 2013 | Posted by Robert Curlette in Lifehacking - (1 Comments)

The best part of sharing is learning. When we share, we invite others to participate in the solution. We need to learn to share, and by sharing we learn from others, with their valuable comments and feedback. It is this feedback loop that we benefit from as a community.

The SDL Tridion MVP award was created to recognize those who share their experiences and knowledge through public blogs, StackOverflow, and other media. I feel honored to be awarded the MVP award for sharing in 2012.

But, this is a new year and the clock resets on the MVP award. We are all now equal and although I shared a lot in 2012 I need to continue sharing in 2013 if I want to have a chance of winning the MVP award again.

With 2013 I welcome all the new bloggers that will contribute more to our community, challenge our ways of thinking, and introduce new ways of solving old problems. The Tridion 2013 product is around the corner. Ask your manager to install it on the Dev machine. Write about the installation, or one of the many new features.

The secret of writing is finding something exciting to write about. If you are excited and curious about a topic, it is much easier to write and also the quality is much better. So, find something interesting, technically, to write about! It could be Tridion, .Net, Java or Web Services. Processes, management or functional designs. But, write a small article and see where it goes! And, if you make mistakes, you can always edit the article and fix them. I know I have!

Speaking of learning, Pluralsight has a great course on ServiceStack (web service framework) and with this link you can get a 1 day free trial. http://pluralsight.com/training/TwitterOffer/seriesa

I wish you all a successful year of learning!

I wanted to create a simple app to store data using the elegant OrmList from ServiceStack.  During this process I found a couple of things that cost me some time and I wanted to share these with you.  In this article I will walk through creating a small and simple ServiceStack OrmLite example.  It should take around 5 minutes to complete the example.

Please see the excellent OrmLite official docs for more examples.

Update Jan 6, 2013:  Code samples updated with the great feedback in the comments from Demis Bellot.

Step 1:  Create a Console App and Reference OrmLite.SQLite

– Create a Console app named OrmLiteExample

– Add the OrmLite references from NuGet (http://www.nuget.org).  Right-click on the References in the Solution Explorer, Manage NuGet Packages, Online, OrmLite.  Or, from the Package Manager Console (View, Other Windows, Package Manager Console) type ‘Install-Package ServiceStack.OrmLite.Sqlite32’.

Step 2:  Create the DTO (aka Model)

This is the nicest part of working with ServiceStack.  Everything is DTO and Model driven – no config files to create and the code is simple and concise.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ServiceStack.DataAnnotations;

namespace OrmLiteExample
{
    class Note
    {
        [AutoIncrement] // Creates Auto primary key
        public int Id { get; set; }

        public string SchemaUri { get; set; }
        public string NoteText { get; set; }
        public DateTime? LastUpdated { get; set; }
        public string UpdatedBy { get; set; }
    }
}

Step 3:  In program.cs, add the Using statements

using ServiceStack.OrmLite;
using ServiceStack.OrmLite.Sqlite;

The using statement ServiceStack.OrmLite.Sqlite will throw an exception if you build now.

Fix:  With NuGet, download and install ServiceStack.OrmLite.Sqlite32 package. Also, go to the properties of your project and make sure the target framework is set to .NET framework 4.  Solved!

Step 4:  Add the connection strings for SqLite

Thanks to the nice Unit Tests in the ServiceStack project I was able to use their example.

public static string SqliteMemoryDb = ":memory:";
// Updated per comment form Jonas.  First connection string below causes an app pool restart!  Use MapHostAbsolutePath instead!
//public static string SqliteFileDb = "~/App_Data/db.sqlite".MapAbsolutePath();  
public static string SqliteFileDb = "~/App_Data/db.sqlite".MapHostAbsolutePath();
Problem:  The MapAbsolutePath is not found.
Fix: Include the missing library:

using ServiceStack.Common.Utils;

Step 5:  Create the connection

//Using Sqlite DB- improved
var dbFactory = new OrmLiteConnectionFactory(
                SqliteFileDb, false, SqliteDialect.Provider);

// Wrap all code in using statement to not forget about using db.Close()
using (var db = dbFactory.Open()) {

Step 6: Create the Table

 

db.CreateTableIfNotExists<Note>();

Step 7:  Insert a record

Note:  This is different than the excellent online examples.  I received a warning message to use the Connection instead of the Command object.

// Insert
db.Insert(
	new Note {
		SchemaUri = "tcm:0-0-0",
		NoteText = "Hello world 5",
		LastUpdated = new DateTime(2013, 1, 5),
		UpdatedBy = "RC" });

Step 8:  Read the data

 // Read
var notes = db.Where<Note>(new { SchemaUri = "tcm:0-0-0" });
foreach (Note note in notes)
{
	Console.WriteLine("note id=" + note.Id + "noteText=" + note.NoteText);
}
Console.ReadLine();

Full Code

Program.cs

Note.cs

This is a small example of how to create a context-menu extension to copy the URI of a Tridion Item.  

Getting Started

The most important and also difficult part of every GUI Extension is the configuration file.  It’s best to start with a previous working example.  For this article I used the classic 8 Steps post from Yoav Nirlan.

Create the Directory

Create a new Folder in:
C:\Program Files (x86)\Tridion\web\WebUI\Editors\CopyUri

Configuration File

I started with the configuration file from the Tutorial and also loaded the Schemas into Visual Studio from Tridion.  This was a very important step because Tridion 2011 SP1 has a slightly different XML Schema for the Context Menu than Yoav’s sample does.  I relied on the intellisenne in Visual Studio and the XML Schema to get a properly formatted Configuration file.  In the future I will start with this updated config file below for Context-menu extensions.

Save it as CopyUri.config in the folder created above.

<?xml version="1.0"?>
<Configuration xmlns="http://www.sdltridion.com/2009/GUI/Configuration/Merge" xmlns:cfg="http://www.sdltridion.com/2009/GUI/Configuration" xmlns:ext="http://www.sdltridion.com/2009/GUI/extensions" xmlns:cmenu="http://www.sdltridion.com/2009/GUI/extensions/ContextMenu">
  <resources cache="true">
    <cfg:filters/>
    <cfg:groups>
      <cfg:group name="Extensions.Resources.CopyUri" merger="Tridion.Web.UI.Core.Configuration.Resources.CommandGroupProcessor" merge="always">
        <cfg:fileset>
          <cfg:file type="script">/Commands/CopyUriCommand.js</cfg:file>
          <cfg:file type="reference">2011Extensions.Commands.CopyUri</cfg:file>
        </cfg:fileset>
        <cfg:dependencies>
          <cfg:dependency>Tridion.Web.UI.Editors.CME</cfg:dependency>
          <cfg:dependency>Tridion.Web.UI.Editors.CME.commands</cfg:dependency>
        </cfg:dependencies>
      </cfg:group>
    </cfg:groups>
  </resources>
  <definitionfiles/>
  <extensions>
    <ext:editorextensions>
      <ext:editorextension target="CME">
        <ext:editurls />
        <ext:listdefinitions/>
        <ext:taskbars/>
        <ext:commands />
        <ext:commandextensions/>
        <ext:contextmenus>
          <ext:add>
            <ext:extension name="CopyUriExtension" assignid="ext_copyuri" insertbefore="cm_refresh">
              <ext:menudeclaration externaldefinition="">
                <cmenu:ContextMenuItem id="ext_CopyUri" name="Copy URI" command="CopyUri"/>
              </ext:menudeclaration>
              <ext:dependencies>
                <cfg:dependency>Extensions.Resources.CopyUri</cfg:dependency>
              </ext:dependencies>
              <ext:apply>
                <ext:view name="DashboardView"/>
              </ext:apply>
            </ext:extension>
          </ext:add>
        </ext:contextmenus>
        <ext:lists/>
        <ext:tabpages/>
        <ext:toolbars/>
        <ext:ribbontoolbars/>
      </ext:editorextension>
    </ext:editorextensions>
    <ext:dataextenders/>
  </extensions>
  <commands>
    <cfg:commandset id="2011Extensions.Commands.CopyUri">
    <cfg:command name="CopyUri" implementation="Extensions.CopyUri"/>
    <cfg:dependencies>
    <cfg:dependency>Extensions.Resources.Base</cfg:dependency>
    </cfg:dependencies>
    </cfg:commandset>
  </commands>
  <contextmenus/>
  <localization/>
  <settings>
    <defaultpage>/Views/Default.aspx</defaultpage>
    <navigatorurl>/Views/Default.aspx</navigatorurl>
    <editurls/>
    <listdefinitions/>
    <itemicons/>
    <theme>
      <path/>
    </theme>
    <customconfiguration/>
  </settings>
</Configuration>

Important parts of the Configuration File

Location of the command in the Context Menu:

insertbefore="cm_refresh"

Text of the menu item:

name="Copy URI"

Important info for the JavaScript in the next part:
1.  Command name
2.  Implementation

<cfg:command name="CopyUri" implementation="Extensions.CopyUri"/>

The command name is also used here:

<cmenu:ContextMenuItem id="ext_CopyUri" name="Copy URI" command="CopyUri"/>

The Commands are in the xml root ‘commands’ node, not the ‘ext:commands’.

 </extensions>
  <commands>
    <cfg:commandset id="2011Extensions.Commands.CopyUri">
    <cfg:command name="CopyUri" implementation="Extensions.CopyUri"/>
    <cfg:dependencies>
    <cfg:dependency>Extensions.Resources.Base</cfg:dependency>
    </cfg:dependencies>
    </cfg:commandset>
  </commands>
  <contextmenus/>

No Ribbon button was added in this demo.

JavaScript Code

1.  Create a new folder ‘Commands’.
2.  Create a new file called CopyUriCommand.js

Type.registerNamespace("Extensions");

Extensions.CopyUri = function Extensions$CopyUri() {
    Type.enableInterface(this, "Extensions.CopyUri");
    this.addInterface("Tridion.Cme.Command", ["CopyUri"]);
};

Extensions.CopyUri.prototype.isAvailable = function CopyUri$isAvailable(selection) {
    return true;
}

Extensions.CopyUri.prototype.isEnabled = function CopyUri$isEnabled(selection) {
    if (selection.getItems().length > 1)
        return false;
    else
        return true;
}

Extensions.CopyUri.prototype._execute = function CopyUri$_execute(selection) {
    selectedItem = selection.getItems()[0];
    prompt("Copy the Item ID using Ctrl/Cmd + C:", selection.getItems()[0]);
}

 Code Explained:

1.  The isEnabled method tells the GUI if the menu option is enabled or disabled.  This extension shows the URI for only one item, so I only enable the extension if one item is selected.
2.  The execute method does the action.  We have available all methods and objects in the Tridion Anguilla framework.  The getItem method returns null but we are lucky that getItems returns whatever is selected in the GUI.  We use the prompt method of JavaScript to provide a nice small window to copy the URI from.

Creating the Virtual Directory in IIS

Create a new Virtual Directory in WebUI/Editors called CopyUri.  This name needs to be the same as the VDIR in the config from the next step.  You may need to update the Security settings for the folder and allow the Network Service user read access.  Use the ‘Test’ button after setting up and you should have a checkmark next to the first test.  The second test will fail and this is normal.

Enabling the Extension

Open the System.config file located at:  <Tridion_home>\web\WebUI\WebRoot\Configuration

Add the following to ‘turn on’ the Extension.  If your Tridion GUI stops working – comment out the following line and it should return to normal.  Double-check your config and settings.

<editor name="CopyUri" xmlns="http://www.sdltridion.com/2009/GUI/Configuration">
	<installpath xmlns="http://www.sdltridion.com/2009/GUI/Configuration">C:\Program Files (x86)\Tridion\web\WebUI\Editors\CopyUri\</installpath>
		  <configuration xmlns="http://www.sdltridion.com/2009/GUI/Configuration">CopyUri.config</configuration>
		  <vdir xmlns="http://www.sdltridion.com/2009/GUI/Configuration">CopyUri</vdir>
</editor>

Copying the WebDav URL

Big thanks to Alex Klock from Tridion Community for answering my question about getting the WebDavURL in Anguilla.  Here is the code we could add to the execute method above to show the WebDav URL instead of URI.

var item = $models.getItem(selectedItem),
    webDavUrl = item.getWebDavUrl();

if (!webDavUrl) {
    // WebDavUrl for cached item hasn't been loaded yet, so lets load it.
    $evt.addEventHandler(item, "loadwebdavurl", function (event) {
        webDavUrl = item.getWebDavUrl(); // also could do event.source.getWebDavUrl()
    });
    item.loadWebDavUrl();
}

Summary

This is a simple GUI Extension but it highlights some basics we need to know when developing a GUI Extension.  The Developer Console in the Chrome Browser was a big help while debugging and writing the JavaScript code.  A big thanks to Hristo Chakarov for his help with the JavaScript development in this post.

If we want to execute some code for Staging but not for the Live Publish Target we need to detect the target title during Publish Time.  In this article I will show how to get the Publish Target Title in VBScript, Razor and C#.

A long long time ago in a land far away….Publication Target Title in VBScript

Before we had the power of .NET we used VBScript (or XSLT) to render our beautiful content from Tridion.  The VBScript rendering engine has the TcmScriptAssistant class to provide handy methods like WriteOut and also objects such as PublicationTarget and PublicationTargetId.  To get the PublicationTarget Title in VBScript we typed:

[% =PublicationTarget.Title %]

Getting the Publication Target Title with a Razor TBB

Today we have the powerful Razor Meditator for rendering our .NET and Razor code since the TcmScriptAssistant is no longer available in our Compound Templates.  So, how do we get the Publication Target Title?

@{
var engine = TridionHelper.Engine;
var publishContext = engine.PublishingContext;
var pubTargetTitle = publishContext.PublicationTarget.Title;
}

Getting the Publication Target Title in a C# TBB

We might be using DWT TBBs and want access to the Publication Target Title.  In this case, we could create a C# TBB, get the Publication Target Title and push it into the Package.

PublishingContext publishContext = engine.PublishingContext;
pubTargetTitle = publishContext.PublicationTarget.Title;
log.Info("PubTargetTitle = " + pubTargetTitle);

Summary
Using the engine object we can get more info about our publishing environment and access properties related to the processing of the item, including publishing context details.   The Publication Target Title is still there and with a bit of looking it is not too hard to find.  Big thanks to Alex Klock for writing the Razor Mediator and keeping up with the bug fixes and feature requests.