February 2008 - Posts
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/