Alright, it's out. I've downloaded it, but not installed yet. I've only just begun reading about the changes and whoah! - way more than I expected!
No more masters? Three kinds of templates? Field types renamed? My head is spinning. I can't wait to play with this stuff and I'll be sure to share my experiences here on the blog.
We have a piece of content on this site we're working on that comes from an outside system instead of Sitecore. The client has requested that when they are previewing the site, this particular piece of content also be shown as a preview. Luckily, the outside system allows us to request the content by date. But I was unsure of how to find out if the site was in preview mode or if it was, what date was being previewed.
Unsure no more! Jens on the SDN foum came through with this extremely simple solution... Sitecore.Configuration.State.Previewing (boolean) and Sitecore.Configuration.State.PreviewDate (DateTime). They do exactly what you'd expect.
No idea how I would have ever found those if it weren't for the SDN forum, though. Thanks, Jens!
Now, I want to do something a bit more dynamic. How about we call a web service, get some dynamic data back, and render something based on that data? In this post, I will build a simple example in order to clearly illustrate the concepts. But at the end, I will post an example of something a bit more useful to show what can be done!
First off, let's define a simple web service. I used VS2008's New Item wizard and selected "Web Service" from the dialog. Then, I replaced HelloWorld() with my own GetData() method. Here's what the code behind for MyService.asmx looks like:
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ToolboxItem(false)]
// To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line.
[System.Web.Script.Services.ScriptService]
public class MyService : System.Web.Services.WebService
{
[WebMethod]
[ScriptMethod]
public string[] GetData()
{
string[] data = new[] { "first data", "second data", "third data" };
return data;
}
}
Note I also have uncommented the ScriptService attribute on my class and added a ScriptMethod attribute to my method. Calling web services from Javascript is a whole lot easier using ASP.NET AJAX. You'll need to have the ASP.NET AJAX 1.0 Extensions installed for this to work, but it's worth it. Oh, I'm also using the new array initialization syntax from C# 3.0 in case you've never seen it before. A real world application would probably query a database or something.
In order to call the web service from Javascript, ASP.NET AJAX requires us to place a ScriptManager on the page with a ServiceReference pointing to the .asmx. Here's what mine looks like:
<asp:ScriptManager ID="ScriptManager1" runat="server">
<Services>
<asp:ServiceReference Path="~/MyService.asmx" />
</Services>
</asp:ScriptManager>
Thankfully, VS2008's web application project template already set up my web.config to use ASP.NET AJAX. If you are not using VS2008, you may need to follow the directions at http://asp.net/AJAX/Documentation/Live/ConfiguringASPNETAJAX.aspx to ensure your web.config is set up properly.
At this point, I can call my web service from Javascript and react to whatever data comes back. The question is - what should it do? For this post, I think I'll just put up a Silverlight TextBlock and rotate through the web service data when the user clicks on the text. Here's the XAML:
<Canvas xmlns="http://schemas.microsoft.com/client/2007" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<TextBlock x:Name="MyTextBlock" FontSize="10" FontFamily="Arial" Canvas.Top="10" Canvas.Left="10" Loaded="MyTextBlockLoaded" MouseLeftButtonDown="MyTextBlockClick" />
</Canvas>
First, I will retrieve the data from the web service once the text block has loaded by using the Loaded event. Let me show you the Javascript and then I will explain it...
var index;
var data;
function MyTextBlockLoaded( sender, args )
{
MyService.GetData( GetDataSuccess, GetDataFailed );
}
function GetDataSuccess( result )
{
data = result;
index = 0;
UpdateTextBlock();
}
function GetDataFailed()
{
alert( 'Call to MyService.GetData() failed!' );
}
function UpdateTextBlock()
{
var textblock = document.getElementById( '<%= Silverlight1.ControlId %>' ).content.findName( 'MyTextBlock' );
textblock.Text = data[index];
}
First I declare a variable to hold the current index into the data array and a variable to hold the data itself. The implementation of the MyTextBlockLoaded() event handler is calling the web service using the proxy built by ASP.NET AJAX. Note that because this is an asynchronous call, you have to pass in event handlers for both success and failure - that is where my GetDataSuccess() and GetDataFailed() functions come from. On success, I store the data, set the initial index to 0, and call the function to set the text in the TextBlock. Note I'm using document.getElementById() to find the Silverlight viewer. I'm using some inline server-side script to get the Id of the Silverlight viewer from my control (covered in a previous post) to pass to getElementById(). The viewer object has a content property upon which I can call findName() (also covered in a previous blog post) to get a reference to the TextBlock.
The last thing to do is handle the MouseLeftButtonDown event on the TextBlock so that the text changes when the user clicks on it. Here's that Javascript function:
function MyTextBlockClick( sender, args )
{
if ( index >= data.length-1 )
{
index = 0;
}
else
{
index = index + 1;
}
UpdateTextBlock();
}
Here, I increment the index and when the index reaches the end of the list, I reset it to 0 to go back to the beginning. Then I call UpdateTextBlock() to display the new text.
You can see this example in action at http://www.killeverything.com/zak/Silverlight1/WebService1.aspx.
At the beginning of this post, I promised to show a more useful example. How about a Silverlight app that displays headlines from an RSS feed and lets you click through to the articles? If you've been reading my Silverlight posts, there shouldn't be anything too new going on here. I have just combined several of the concepts I've been learning and blogging about into a single Silverlight app. You can see it running here: http://www.killeverything.com/zak/Silverlight1/WebService2.aspx and the code for both the simple example and the RSS headline viewer are attached to this post. If you're a .NET head, you might want to look at the source for the web service where I used LINQ to XML to make parsing the RSS feed easy as cake. Oh, and I want to point out the clipping I had to add to the TextBlock. Support for MaxHeight and MaxWidth may be coming in Silverlight 2.0, but clipping is how it's done for now...
Putting a Silverlight viewer on a web page is a lot of work. You have to import the Silverlight.js file (and make sure it's deployed to the web server), create a div to contain the viewer, and write some Javascript code to call Silverlight.createObject(). To me, this is just begging for an ASP.NET control - so I whipped up one of my own.
So what do I want my control to do for me? Let's start at the top with the Silverlight.js file. I want this to be an embedded resource so that I don't always have to remember to copy it in to a new project. If you're not familiar with this technique, it's been around since .NET 2.0 - keep up! :) You just throw the Silverlight.js file into your project and set it's build action to "Embedded Resource". Here's a screenshot:
Add it to your project's AssemblyInfo.cs like this:
// Embeded Resources
[assembly: System.Web.UI.WebResource("knw.Silverlight.Silverlight.js", "application/x-javascript")]
Then my control's code tells the page where to find it like this:
Page.ClientScript.RegisterClientScriptInclude("Silverlight.js", Page.ClientScript.GetWebResourceUrl(this.GetType(), "knw.Silverlight.Silverlight.js"));
Here is a link to an article on Code Project that explains the technique in a bit more detail.
Next, I want my control to render the container div for the Silverlight viewer. This is pretty straightforward - I just override Render() and use the HtmlTextWriter that's passed in. Here's what it looks like:
protected override void Render(HtmlTextWriter writer)
{
base.Render(writer);
writer.AddAttribute("id", ID + "Div");
writer.RenderBeginTag(HtmlTextWriterTag.Div);
writer.RenderEndTag();
}
Note I'm just concatenating "Div" onto the existing ID of my control when building the div's id, but that should make it unique.
The only thing that's left is the call to Silverlight.createObject(). There are a few variables in this call that I thought could be exposed as properties of my control. Specifically, the path to the Xaml document, the height and width of the viewer, and the background color. I'm using C# 3.0, so I took advantage of another language feature - automatic properties. Here's what that code looks like:
public string Xaml { get; set; }
public int Height { get; set; }
public int Width { get; set; }
public Color BackgroundColor { get; set; }
Lastly, I need to build the actual javascript to create the Silverlight viewer and register it with the page to run as a startup script. Like this:
string createObjectScript = string.Format("<script>Silverlight.createObject( \"{0}\", document.getElementById( \"{1}Div\" ), \"{2}Ctrl\", {{ width:'{3}', height:'{4}', inplaceInstallPrompt:false,background:'{5}', isWindowless:'false', framerate:'24', version:'1.0' }},{{ onError:null,onLoad:null }},null );</script>", Xaml, ID, ID, Width, Height, ColorTranslator.ToHtml(BackgroundColor) );
Page.ClientScript.RegisterStartupScript( this.GetType(), ID + "Create", createObjectScript );
At first glance, that code looks pretty confusing. All it's actually doing is using string.Format() to build the call to Silverlight.createObject(). {0} is the path to the Xaml, {1} is the ID of the control + "Div" (the id of container div), {2} is the ID of the control + "Ctrl" (a unique id for the viewer itself), {3} is the width, {4} is the height, and {5} is the background color (converted from a System.Drawing.Color using the System.Drawing.ColorTranslator class). Oh yeah, and you have to escape curly braces when using string.Format() by doubling them.
Ok, that was a lot of work - what has it bought me? Well, now when I want to use Silverlight on an .aspx page, all I have to do is add a reference to my DLL, register it at the top of the .aspx page, and put the control on the .aspx page somewhere. Which looks like this:
<knw:Silverlight id="Silverlight1" runat="server" Xaml="MySilverlight.xaml" BackgroundColor="Gray" Height="200" Width="200" />
I will attach the code for both the control and an example of using it to this post. I'll be using this control in future Silverlight posts here - plus, I do intend to extend it to do more in the future (as I learn more about Silverlight). So, I'll try to post updates here from time to time. I hope it's useful to someone! :)
Seems I've fallen way behind on my Silverlight blogging as of late. A few weeks ago, I managed to get some simple animation working. So, I guess I should blog about it.
Animation is done by transforming your object. Each graphic element has a RenderTransform property to which you can assign a Transform object to modify your element in some way. The different types of Transform objects include RotateTransform, SkewTransform, ScaleTransform, and TranslateTransform. Let's look at an example.
<Ellipse MouseLeftButtonDown="EllipseClick" Height="50" Width="100" Canvas.Top="50" Canvas.Left="50" Fill="Black">
<Ellipse.RenderTransform>
<RotateTransform x:Name="EllipseXForm" Angle="90" />
</Ellipse.RenderTransform>
</Ellipse>
This example is pretty boring - it just renders a 50 pixel high and 100 pixel wide ellipse, rotated 90 degrees (so that it instead appears 100 pixels high and 50 pixels wide). However, it illustrates the effect that a RotateTransform will have on an object. Also note that I have named the transform (the x:Name attribute). The purpose of that will soon become apparent.
Animations are defined within objects called Storyboards. Basically, a Storyboard defines a timeline of changes that should occur to properties of objects. For my example, I want to change the Angle property of the RotateTransform object over time (so that the Ellipse rotates slowly as we watch). Here is the Storyboard I have defined:
<Canvas.Resources>
<Storyboard x:Name="EllipseStoryboard">
<DoubleAnimation Storyboard.TargetName="EllipseXForm" Storyboard.TargetProperty="Angle" To="0" Duration="00:00:05" />
</Storyboard>
</Canvas.Resources>
For this example, I have used a DoubleAnimation. This is because the property I want to change ("Angle") is of type double. There are many other animation objects available such as DecimalAnimation, Int32Animation, and ColorAnimation. The Storyboard.Targetname attribute tells the animation which object it is changing (here's where I use the name I gave my transform earlier) and Storyboard.TargetProperty indicates what property will be changed. The To attribute is the value the animation should end with and the Duration should be self-explanatory. Note again, I have named my Storyboard so that I can reference it later. Oh yeah, and the Storyboard is a resource, so it's defined inside the <Canvas.Resources /> element.
I'm going to need something to trigger the animation to begin. I've chosen to do it using a mouse click on the ellipse. So, I will need to add a MouseLeftButtonDown handler to the Ellipse. Now it looks like this:
<Ellipse MouseLeftButtonDown="EllipseClick" Height="50" Width="100" Canvas.Top="50" Canvas.Left="50" Fill="Black">...
And I need an EllipseClick() function defined in Javascript, so here's what I came up with:
<script type="text/javascript">
function EllipseClick( sender, args )
{
sender.findName("EllipseStoryboard").Begin();
}
</script>
I placed this directly in my .aspx file for this example. I don't think I've blogged about findName() before, but Silverlight elements define this method and it allows you to get a reference to any other Silverlight element by name. Here, I am using it to get the "EllipseStoryboard" Storyboard and executing it's Begin() method.
You can see this example in action at http://www.killeverything.com/zak/Silverlight1/EllipseRotate1.aspx
It's not quite what I wanted, though. The rotation is happening around the top left corner of the ellipse instead of the center. Luckily, this was easy to fix - the RotateTransform has CenterX and CenterY properties that allow you to define the point around which the object rotates. Knowing that the ellipse is 100 wide and 50 tall, this is what I changed the transform to look like:
<RotateTransform x:Name="EllipseXForm" Angle="90" CenterX="50" CenterY="25" />
You can see the finished example at http://www.killeverything.com/zak/Silverlight1/EllipseRotate2.aspx
I've also attached a ZIP with the files needed for these examples in case you want to play with them yourself.
It seems several people still haven't heard about Crestone, so thought I'd make a quick mention of it here... The new Sitecore release (due out mid-year, hopefully) is code-named Crestone. The beta program is just spinning up, but a few details have leaked out already. 64-bit support is one of the big features. Also, in-line editing in web preview mode (I've seen this in other CMS's before and it's a huge selling point with customers), a new page layout tool, and a whole new security engine.
Yes, I'm going to apply for the beta program. If I'm accepted, I won't be able to blog about it for awhile though, unfortunately. As soon as I have more information to share though, I will!
For a project I'm currently working on, the client wanted to have a multilist field on one of their templates but have the list of items displayed on the "All" side filtered according to some complex rules. Normally, you can filter that list by assigning an XPath query to the Source property of the field. In this case though, the filtering rules were going to be very difficult to implement with XPath and I felt some .NET code was in order.
The solution I chose was to build a custom field type, derive it from the existing MultiList field type, and place my filtering therein. Turns out all I had to do was override the GetItems method on Sitecore.Shell.Applications.ContentEditor.MultilistEx, call the base implementation to get the initial list of items, then iterate through them - discarding the ones I don't want before returning the list.
Here is a full example using the workflow state code I posted a couple of weeks ago:
public class FilteredMultiList : Sitecore.Shell.Applications.ContentEditor.MultilistEx
{
protected override Item[] GetItems(Item current)
{
List<Item> filteredItems = new List<Item>();
// Call the base class to get the original list.
Item[] items = base.GetItems(current);
// Loop through the items and filter them.
foreach (Item item in items)
{
// Get the master database.
Database masterDatabase = Factory.GetDatabase("master");
// We want to exclude items that are not in the final workflow state...
IWorkflow workflow = masterDatabase.WorkflowProvider.GetWorkflow(item);
WorkflowState state = workflow.GetState(item);
if (state == null) continue;
if (!state.FinalState) continue;
// If we make it this far, add the item to the filtered list.
filteredItems.Add(item);
}
// Return the filtered list.
return filteredItems.ToArray();
}
If you've never done a custom field type before, installation requires two steps... First, edit web.config and search for the <controlSources> section. Add an entry for the assembly and namespace containing your field types. For example:
<source mode="on" namespace="Your.Namespace" assembly="Your.Assembly" prefix="Prefix" />
What this does is associates the prefix "Prefix" (you can use any name you want) with all field types in Your.Namespace inside Your.Assembly.dll. You'll see in a second how this prefix is used.
Now, in content editor - expand the System node, right-click on Field Types, select New -> Add From Template. Expand the System folder, Templates folder, and select the "Template field type" template. Name the field type whatever you want it to show up when editing a template in template manager.
Fill in the Control field using Prefix:Class syntax where Prefix is the prefix used in web.config and Class is the name of your class that implements this field type.
Now you can go edit a template and add a field using your new field type. Set the source as usual, but when the user edits an item based on your template, the new field will filter the source list using whatever code you placed in the overridden GetItems!
Today's goal is to display an image and react to a mouse click in some way. I actually started by reading the QuickStart that gets installed along with the Silverlight SDK. You can also find it at http://silverlight.net/quickstarts/silverlight10/default.aspx if you still don't have the SDK installed (and why don't you? Huh?)
It looks like all I need to display an image is an object set to fill with an ImageBrush. Something like this should work:
<Ellipse Height="100" Width="200" Canvas.Top="10" Canvas.Left="10">
<Ellipse.Fill>
<ImageBrush ImageSource="lolcat_quadcore.jpg" />
</Ellipse.Fill>
</Ellipse>
Bingo, that did the trick. It looks like this:
Oh wait, I just found the Image object - no brush needed if you do it like this:
<Image Source="lolcat_quadcore.jpg" Canvas.Top="120" Canvas.Left="10" Height="100" />
Which looks like this:
Note that in the first case, the image was automatically stretched to fit the dimensions of the ellipse and in the second, I only specified a height - so the image was automatically scaled proportionally. Cool!
Now, processing mouse clicks. This appears to be amazingly easy as well. First, you need to declare the event handler in the XAML mark-up like this:
<Image Source="lolcat_quadcore.jpg" Canvas.Top="120" Canvas.Left="10" Height="100" MouseLeftButtonDown="ChangeLolcat" />
Then you just need a javascript function to handle the event - mine looks like this:
function ChangeLolcat( sender, args )
{
if ( sender.Source == "lolcat_quadcore.jpg" )
sender.Source = "lolcat_schroedinger.jpg";
else
sender.Source = "lolcat_quadcore.jpg";
}
In this case, because the MouseLeftButtonDown event is on the Image object, that's the object that gets passed in to the javascript function as "sender". I am simply changing the Source property from how it was defined in the XAML in response to the mouse click. And it works! (sorry I can't show you - I don't have a good place to host .NET code at the moment) I think I'm going to like working with Silverlight.
In case you want to download the code for this project and try it out yourself, I will ZIP it up and attach it to this blog post.
Ok, I fell for the hype and decided to try out Live Writer. If this works like I expect, I will be pretty impressed. Let me try some stuff...
| table1,1 | table2,1 | table3,1 | table4,1 |
| table1,2 | table2,2 | table3,2 | table4,2 |
Pictures...


Alright, this is definitely easier than authoring inside Community Server itself. I'm sold. Oh, and thanks Google Images for the lolcats. :)
So I finally set out to do something with Silverlight... I set a simple goal for myself tonight - I just wanted to see "Hello World". That should be easy, right?
First, I downloaded the Silverlight 1.0 SDK from http://www.microsoft.com/silverlight and installed it.
Second, I created a new web site in Visual Studio.
Third, I grabbed a copy of Silverlight.js and added it to the web site. For me, it was in C:\Program Files\Microsoft Silverlight 1.0 SDK\Tools\Silverlight.js.
I added a link to Silverlight.js in the HTML head of default.aspx. Like this:
<script type="text/javascript" src="Silverlight.js">
</script>
Next, I added a div in the HTML body of default.aspx and gave it an id. It doesn't need the runat=server attribute, just an id (it will only be reference from Javascript). Like this:
<div id="MySilverlightDiv">
</div>
Next, I created a XAML file. I know next to nothing about XAML yet, so I just used Google to "borrow" someone else's example code for now. My XAML file is called MySilverlight.xaml and looks like this:
<Canvas xmlns="http://schemas.microsoft.com/client/2007" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<TextBlock FontSize="40" FontFamily="Arial" Canvas.Top="20" Canvas.Left="20">
Hello World!
</TextBlock>
</Canvas>
Last thing I did was add the javascript to my HTML file to actually create the Silverlight object in my div:
<script type="text/javascript">
Silverlight.createObject(
"MySilverlight.xaml",
document.getElementById( "MySilverlightDiv"),
"MySilverlightControl",
{ width:'400',
height:'200',
inplaceInstallPrompt:false,
background:'#DDDDDD',
isWindowless:'false',
framerate:'24',
version:'1.0' },
{ onError:null,
onLoad:null },
null );
</script>
That's the filename of my XAML file, the Id of my div, a unique Id for the Silverlight control itself, and some various parameters for Silverlight that I do not yet fully understand.
That's it. I hit my web site with a browser and there was my awesome Hello World!
Hopefully tomorrow I will have time to research XAML some more and do something a bit more interesting with Silverlight. I hope to extend this simple starter project and continue writing about it here, so if you're interested - copy/paste the code above and come back tomorrow! :)
Something I was working on today required me to figure out a way to determine the current workflow state for a Sitecore item. Note, this code is running within the context of the content editor (a custom field type, to be exact). Obviously, an item in the web database had better be in the "Published" state of the workflow. :)
As usual, digging around on the SDN5 site didn't help me out much, but eventually I figured this out:
Database masterDatabase = Factory.GetDatabase("master");Item currentItem = masterDatabase.Items["/sitecore/content/Home"];
IWorkflow workflow = masterDatabase.WorkflowProvider.GetWorkflow(currentItem);
WorkflowState state = workflow.GetState(currentItem);
In this code, I'm getting a reference to the master database (since I'm working with an unpublished item), getting a reference to my site's homepage (just an example - in my real world code I already have an item reference), and then using GetWorkflow() and GetState() methods I found whlie exploring the Sitecore APIs.
In any case, this seems to work for me. In my case, I need to know if the item is in the last state in the workflow, so I am checking if state.FinalState is true.
Someone asked me today if I ever got around to downloading and installing the new Sitecore Xpress. I did indeed! My first impression? It's just the Sitecore SBSK (Small Business Starter Kit) re-packaged with a nice "free for personal use" license slapped on top. Kind of underwhelming, but don't get me wrong - it's still a really good thing.
For those not familiar with the SBSK, it provides a nice sample site for you to begin with when building your first site with Sitecore. Example templates, layouts, renderings, CSS, etc are all included along with some instructions to help get you off the ground.
Here are some quick screenshots I took of my Sitecore Xpress install. The first shows the initial published site that comes with the package. The second is the SBSK sample site that also comes with the package. The third shows some of the pre-built templates that are included.
If you want to read the full Sitecore Xpress license, it can be found at http://www.sitecorexpress.net/sitecore/content/Express/LicenseAgreement.aspx. And of course if you want to download it yourself, head ovr to http://www.sitecorexpress.net/
Something I have done a few times in Sitecore is to have new news articles or blog posts automatically organize themselves within year/month folders in my content tree. The URL to a specific news article might be /news/2008/02/ActionHandler.aspx for instance. When I create the ActionHandler content item, the 2008 folder and the 02 folder would be created automatically if they did not already exist and my posting would be placed under these folders for me. In Sitecore, I do this with an event handler for item:creating. Tonight, I set out to do the same thing with Umbraco.
First, let me present an example of what I want to see when I'm done...
I want to be able to right-click on "Blog" and create a new document of type "BlogPost". Given today's date, I want a year folder and a month folder to be created (if they don't already exist) and my new post moved into the proper folder automatically.
A bit of research uncovered Umbraco's concept of Action Handlers. Basically, these are your own classes you can use to perform custom actions when certain events occur in Umbraco's content editor. They just have to implement the umbraco.BusinessLogic.Actions.IActionHandler interface and exist in an assembly in the Umbraco bin folder.
So, I set out to implement my DateFolderActionHandler class... I created a new class library project, added references to Umbraco's businesslogic.dll, cms.dll, and interfaces.dll, and added a class that implements IActionHandler. Visual Studio was kind enough to fill in a skeleton implementation for me - here's what it looked like:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using umbraco.BusinessLogic.Actions;
using umbraco.BusinessLogic.console;
using umbraco.cms.businesslogic.web;
namespace Demo
{
class DateFolderActionHandler : IActionHandler
{
#region IActionHandler Members
public bool Execute(Document documentObject, umbraco.interfaces.IAction action)
{
throw new NotImplementedException();
}
public string HandlerName()
{
throw new NotImplementedException();
}
public umbraco.interfaces.IAction[] ReturnActions()
{
throw new NotImplementedException();
}
#endregion
}
}
The first method that grabbed my attention was HandlerName() - all I have to do is return a string! I did some digging with Reflector and it doesn't appear to be used for anything important, so return anything you want from this method. I used "DateFolderActionHandler" - the name of my class.
Next up - ReturnActions(). This appears to return a list of actions your handler wants to be notified of. The actions you can choose from are members of the umbraco.BusinessLogic.Actions namespace. My handler needs to execute when a new content item is created, so I'm using ActionNew(). ReturnActions() now looks like this:
public umbraco.interfaces.IAction[] ReturnActions()
{
return new umbraco.interfaces.IAction[] { new umbraco.BusinessLogic.Actions.ActionNew() };
}
Last to implement is the Execute() method. I'm guessing Execute() gets called when one of the actions you have subscribed to occurs. The Document must be the content item that was acted upon and the IAction must be the action that was triggered. In my case, I am only subscribing to ActionNew so I don't have to check the action, but I will in this example to demonstrate how it's done.
// Not interested in anything but "create" events.
if (action.Alias != "create") return true;
My action handler will be called for every new content item that is created, so the first thing I will want to do is verify that this document is one that I'm interested in. I checked the content type like this:
// Not interested if the item being added is not a blog post.
if (documentObject.ContentType.Alias != "BlogPost") return true;
Note that
I am returning true on both of these cases.
My assumption was that the return value from Execute() indicated if the
action should be allowed to continue or not.
After playing around with returning false (and peeking at the code with
Reflector), it appears the return value is just ignored. Shame on Umbraco - there does not appear to
be any way to cancel an action! Even
throwing an exception here doesn't keep the action from happening. Anyway - I will continue to return true when
I want the action to complete and false if I don't in hopes that it will
someday make a difference. :)
So if my code is still executing, I know I have a new document that I'm interested in
placing in date folders. I want a folder
for the year and a folder for the month, so I'll start with the current date.
string year = DateTime.Now.ToString("yyyy");
string month = DateTime.Now.ToString("MM");
As I
mentioned at the start of this article, I expect the new document to be created
as a child of "Blog". The year
folder should also exist as a child of "Blog". My first task is to check if that already
exists. Here's the code I used:
Document yearDocument = null;
foreach (IconI child in documentObject.Parent.Children)
{
if (child.Text == year)
{
yearDocument = new Document(child.Id);
break;
}
}
(note - I
have no idea why the members of documentObject.Parent.Children are of type
"IconI". Maybe someone that
knows Umbraco better can answer that?)
What this does is get the new item's parent's children (children of "Blog") and
loops through them looking for one whose name matches the current year. If we make it through this loop and
yearDocument is still null, we know the folder doesn't exist and needs to be
created. Here is how I handle that:
// If the year folder doesn't exist, create it.
if (yearDocument == null)
{
yearDocument = Document.MakeNew(year, DocumentType.GetByAlias("YearFolder"), documentObject.User, documentObject.Parent.Id);
}
The Document.MakeNew() call creates a new content item named for the current year,
using the document type "YearFolder", created by the same user that
created the blog post, underneath the parent of the blog post
("Blog").
Now I
have to repeat the same process for the month folder...
Document monthDocument = null;
foreach (IconI child in yearDocument.Children)
{
if (child.Text == month)
{
monthDocument = new Document(child.Id);
break;
}
}
// If the month folder doesn't exist, create it.
if (monthDocument == null)
{
monthDocument = Document.MakeNew(month, DocumentType.GetByAlias("MonthFolder"), documentObject.User, yearDocument.Id);
}
And the
final step is to move the new content item into the month folder...
// Move the document into the month folder.
documentObject.Move(monthDocument.Id);
Then I
can just return true and I'm done!
If you want to see the final DateFolderActionHandler.cs source file, I have attached it to this blog post.
Sitecore Xpress launches tomorrow!
http://xpress.sitecore.net/
One of the DNM guys asked this morning if we could limit the "What's New" section on the homepage to display only blog posts (no forum posts, recent photos, files, etc). I figure there is probably a way to filter the data being bound to the IndexPostList control on the homepage, but a simpler way for me was to copy/paste over the existing WeblogPostList control from the Blogs page. After applying a little CSS magic, I came up with this:
<CSBlog:WeblogPostList runat="Server">
<QueryOverrides PagerID="Pager" IsAggregate="true" />
<HeaderTemplate>
<p />
<h2 class="CommonTitle"><CSControl:ResourceControl ResourceName="default_homepage_recentposts" runat="server" /></h2>
<div class="CommonContent">
<ul class="CommonSearchResultList">
</HeaderTemplate>
<ItemTemplate>
<CSControl:ConditionalContent runat="server">
<ContentConditions Operator="Not"><CSBlog:WeblogPostPropertyValueComparison runat="server" ComparisonProperty="IsExternal" Operator="IsSetOrTrue" /></ContentConditions>
<TrueContentTemplate><li class="BlogPostArea"></TrueContentTemplate>
<FalseContentTemplate><li class="BlogPostArea External"></FalseContentTemplate>
</CSControl:ConditionalContent>
<div class="CommonSearchResultArea Weblog">
<CSBlog:WeblogPostData Property="Subject" LinkTo="Post" Tag="H4" CssClass="CommonSearchResultName" runat="server" />
<CSBlog:WeblogPostData Property="Excerpt" Tag="Div" CssClass="CommonSearchResult" runat="server" />
<div class="CommonSearchResultDetails">
<CSBlog:WeblogPostData Property="UserTime" LinkTo="Post" IncludeTimeInDate="true" runat="server" />
<CSControl:ResourceControl runat="server" ResourceName="Weblog_Aggregate_By" />
<CSBlog:WeblogPostData Property="DisplayName" LinkTo="AuthorUrl" runat="server" />
<CSControl:ResourceControl runat="server" ResourceName="To" />
<CSBlog:WeblogData Property="Name" LinkTo="HomePage" runat="server" />
<CSBlog:WeblogPostTagEditableList runat="server" EditorLinkCssClass="CommonTextButton" EditorCssClass="CommonInlineTagEditor" Tag="Div" />
</div>
</div>
</li>
</ItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</CSBlog:WeblogPostList>
I replaced the IndexPostList control in home.aspx with this and removed the code from OnInit that bound data to the post list and that's all there was to it!
More Posts
Next page »