Using WSS v3 for a database

As many of you know I am on the board of CodeMash. We are now working on updating the site for this year's conference.  We just pushed v1 live, and are now working on the attendee registration features.

Last year we had a great site. It was ASP.NET, web services built with the service software factory, and SQL Server 2005. It worked great.

One of the features for the site was a news channel that was on the homepage, a news page, and an rss feed. In order to manage the news content we also had an admin tool, and some supporting tables and so on.

This worked fine.

But this year, as I was refactoring, I just didn't want to deal with remote DBs, and upgrading the admin interface. So, one day while I was talking to local WSS bad boy (amongst other things), I came up with the idea of having the news content come from a WSS list that was on our planning portal.

Sounded easy to me; wire the news business object to the web services for WSS instead of some EntLib code that I had in place, and we're done. Check it in, go to bed.

In theory, it's that easy. I ran into the dreaded Loosey-Goosey anti-pattern that most of the WSS web services are based on. I don't know how I would do it better, but it still is Loosey-Goosey.

You see, many of them take a fragment of XML for a parameter, it isn't strongly typed in anyway. So the consumer has to munge together this XML (which is really CAML), and submit it in, and hope it is right. And if you screw up the official name for a list, view, or column (versus the displayed name), you really don't get much in the way of helpful information in the exception. You get 'field not found', which doesn't tell you which of the 20 fields you queried for wasn't found. Frankly, it made me miss the days of classic asp and vb6 com with 'Internal Server Error 500".

I want to share how I eventually figured out how to store and retrieve the news content form a list in WSS for our site. I don't know if this is the best way to do it, but it works for me.

Also, as a side note, I eventually found a tool that helps you write the CAML. It is published by U2U here. It even inspects your WSS list to know what all of the column names are. There is a library you can reference in your .NET application that loads the generated CAML (and 'connection string') so that you can abstract the WSS calls away from your code. This is a really cool idea. Unfortunately while I was able to use it to help me craft the CAML, I couldn't get the library to work. It might be because I am using Orcas Beta 2.

I started by adding a reference to the Lists web service for our WSS server. I had to provide username/password credentials to get through security. The URL I used was in this form:

http://<<site name>>/_vti_bin/lists.asmx

I did this with the normal 'add web reference' process in VS. Remember that the URL for WSS services is virtualized to the subsite that you want to access. So if the root had a collection of sites, and you wanted to query a list in a site in the collection, you would have to put that in the URL of the service: http://wss.mysite.com/subsite/_vti_bin/lists,asmx.

Once you have the reference, you can create an instance of the proxy class. You then have to add the credentials you want the proxy to use to access the list. You will need a 'using System.Net;' for the NetworkCredential namespace.

org.CodeMash.wss.Lists listService = new org.CodeMash.wss.Lists();
listService.Credentials = new NetworkCredential("WSSUsername", "WSSpassword");

You then need to craft several pieces of CAML. The first is the query expression. This is similar to a filter statement in SQL. Then you need a fragment that represents the columns/fields you want returned. The below statement is like this SQL:

select Title, ID, Created, Expires, Body, Summary From Listname where Expires > "07-15-2007" order by Created

  Here is the code:

XmlDocument xmlDoc = new System.Xml.XmlDocument();

XmlNode ndQuery = xmlDoc.CreateNode(XmlNodeType.Element, "Query", "");

XmlNode ndViewFields = xmlDoc.CreateNode(XmlNodeType.Element, "ViewFields", "");

XmlNode ndQueryOptions = xmlDoc.CreateNode(XmlNodeType.Element, "QueryOptions", "");

ndViewFields.InnerXml = "<FieldRef Name='Title' /><FieldRef Name='ID' /><FieldRef Name='Created' /><FieldRef Name='Expires' /><FieldRef Name='Body' /><FieldRef Name='Summary' />";

ndQuery.InnerXml = "<OrderBy><FieldRef Name=\"Created\" Ascending=\"False\" /></OrderBy><Where><Gt><FieldRef Name=\"Expires\"/> <Value Type=\"DateTime\">2007-07-15T00:00:00</Value></Gt></Where>";

Then, I create a generic collection of the business objects I am going to create. I plan on creating one NewsItem object for each item in the news list returned. After that I call the 'GetListItems' method on the List Service. You need to pass in the name of the list, the view to query against (which is optional), and the query expression, fields to be returned, and the query options. There are a few other optional parameters. You should go look up the full spec if you need to know all the details. That method is going to return a bunch of CAML. I use XMLNode to iterate over the collection of nodes, creating a new NewsItem object using the new C# 3.0 constructor syntax. This is really handy I have to say. You can see that I have to cast each value to the real type I want. Also, each field in the list is returned as an attribute on the item element in the CAML. After that I just return the generic collection of NewsItems back to the caller.

List<NewsItem> headlines = new List<NewsItem>();

                         XmlNode ndListItems = listService.GetListItems("Site News Feed", null, ndQuery, ndViewFields, null, null, null);

               String xpq = "//*[local-name() = 'data' and namespace-uri() = 'urn:schemas-microsoft-com:rowset']/*[local-name() = 'row' and namespace-uri() = '#RowsetSchema']";
               XmlNodeList nodes = ndListItems.SelectNodes(xpq);
               foreach (XmlNode aNode in nodes)
               {
                   headlines.Add(new NewsItem
                   {
                       CreateDate = Convert.ToDateTime(aNode.Attributes["ows_Created"].Value),
                       ExpireDate = Convert.ToDateTime(aNode.Attributes["ows_Expires"].Value),
                       Headline = aNode.Attributes["ows_Title"].Value,
                       NewsBody = aNode.Attributes["ows_Body"].Value,
                       NewsId = Convert.ToInt32(aNode.Attributes["ows_ID"].Value),
                       Summary = aNode.Attributes["ows_Summary"].Value
                   });
               }

Once a user clicks on a headline, I need to fetch the complete news article to display in a popup window. About the only thing that changes is the CAML on the query, and that I am receiving one record back, and not a collection. In this case, I am going to query on the item ID that WSS has, sort of like a primary key in a database.

public static NewsItem GetNewsItem(Int32 itemId)
        {
            org.codemash.wss.Lists listService = new org.codemash.wss.Lists();
            listService.Credentials = new NetworkCredential("WSSUsername", "WSSPassword");

            XmlDocument xmlDoc = new System.Xml.XmlDocument();

            XmlNode ndQuery = xmlDoc.CreateNode(XmlNodeType.Element, "Query", "");
            XmlNode ndViewFields = xmlDoc.CreateNode(XmlNodeType.Element, "ViewFields", "");
            XmlNode ndQueryOptions = xmlDoc.CreateNode(XmlNodeType.Element, "QueryOptions", "");

            ndViewFields.InnerXml = "<FieldRef Name='Title' /><FieldRef Name='ID' /><FieldRef Name='Created' /><FieldRef Name='Expires' /><FieldRef Name='Body' />";

            ndQuery.InnerXml = "<Where><Eq><FieldRef Name=\"ID\" /><Value Type=\"Counter\">" + itemId + "</Value></Eq></Where>";

             XmlNode ndListItems = listService.GetListItems("Site News Feed", null, ndQuery, ndViewFields, null, null, null);

                String xpq = "//*[local-name() = 'data' and namespace-uri() = 'urn:schemas-microsoft-com:rowset']/*[local-name() = 'row' and namespace-uri() = '#RowsetSchema']";
                XmlNodeList nodes = ndListItems.SelectNodes(xpq);
                XmlNode aNode = nodes[0];
               NewsItem aNewsItem = new NewsItem
                    {
                        CreateDate = Convert.ToDateTime(aNode.Attributes["ows_Created"].Value),
                        ExpireDate = Convert.ToDateTime(aNode.Attributes["ows_Expires"].Value),
                        Headline = aNode.Attributes["ows_Title"].Value,
                        NewsBody = aNode.Attributes["ows_Body"].Value,
                        NewsId = Convert.ToInt32(aNode.Attributes["ows_ID"].Value)
                    };
                       return aNewsItem;
        }

The great thing is I don't have to write an admin console, or anything like that. The tool is already there for me in WSS. The news authors can now go in there and control the content. Add items, remove items, stage them for future publication. All with very little work for me.

I implemented the same concept for our 'Topic Submission' form. This is where speakers can submit topics for consideration. In this case, I have a normal web form. Once I validate and control all of the input, instead of writing into a database, I write it to a list in WSS. Now the team that is managing content can easily see what topics have been submitted. With WSS' great filtering and views they can create views that are broken out by tracks, or author, or approval status. The code is below (some stuff was ripped out to make it simpler.) The tricky part is knowing what the field names really are. You have to spelunk the list design form and look at the URL in the browser to get these. For example, my field on the list I created called SpeakerBio is really named 'Speaker_x0020_Bio' by WSS. I did some REAL basic-hacky-ill advised-don't do this at home exception checking at the end. (Yeah, and I know I should have used a string builder. so sue me. this code is going to be run about 200 times, and that's it, and performance isn't an issue.)

private void SubmitATopic(string SessionTitle, string SessDesc, string SpeakerName, string SpeakerEmail, string SpeakerPhone, string SpeakerBio, string Track, int PrevGiven)
        {
            string ListId = "ListGUID";

            org.codemash.wss.Lists listService = new org.codemash.wss.Lists();
            listService.Credentials = new NetworkCredential("WSSUsername", "WssPassword");

            string strBatch = "<Method ID='1' Cmd='New'>" +
                "<Field Name='Title'>" + SessionTitle + "</Field>" +
                "<Field Name='Session_x0020_Description'>" + SessDesc + "</Field>" +
                "<Field Name='Speaker_x0020_Name'>" + SpeakerName + "</Field>" +
                "<Field Name='Speaker_x0020_Email'>" + SpeakerEmail + "</Field>" +
                "<Field Name='Phone_x0020_Number'>" + SpeakerPhone + "</Field>" +
                "<Field Name='Speaker_x0020_Bio'>" + SpeakerBio + "</Field>" +
                "<Field Name='Track'>" + Track + "</Field>" +
                "<Field Name='UniqueID0'>" + System.Guid.NewGuid().ToString() + "</Field>" +
                "<Field Name='Session_x0020_Previously_x0020_P'>" + PrevGiven.ToString() + "</Field>" +
                "</Method>";

            XmlDocument xmlDoc = new System.Xml.XmlDocument();

            System.Xml.XmlElement elBatch = xmlDoc.CreateElement("Batch");

            elBatch.SetAttribute("OnError", "Continue");

            elBatch.InnerXml = strBatch;

            XmlNode ndReturn = listService.UpdateListItems(ListId, elBatch);

            if (ndReturn.InnerText != "0x00000000")
                throw new System.Exception(ndReturn.InnerText);
        }

Anyway, this works fairly well, and saved me from having to build a whole bunch of backend infrastructure. Which is good, because I had to focus on reading the last Dune book. But that is a whole different post.

Tags:

Comments

poceni okna Slovenia
2/23/2010 5:44:45 AM Permalink

Useful and nice information. I am going to subscribe your blog. Thnx.

play whack your boss Turkey
3/8/2010 1:13:02 PM Permalink

Superb article!!

aussie chat Australia
3/12/2010 4:10:44 PM Permalink

Good afternoon, This page is very informative and fun to read. I am a huge follower of the things blogged about. I also love reading the comments, but it seems like a great deal of readers need to stay on topic to try and add something to the original topic. I would also encourage all of you to bookmark this page to your most used service to help get the word out. Thanks

wow leveling guide People's Republic of China
3/17/2010 4:11:19 PM Permalink

Pretty good post. I just stumbled upon your blog and wanted to say that I have really enjoyed reading your blog posts. Any way I'll be subscribing to your feed and I hope you post again soon.

Pretty good post. I just stumbled upon your blog and wanted to say that I have really enjoyed reading your blog posts. Any way I'll be subscribing to your feed and I hope you post again soon.

Jamorama Review United States
3/23/2010 5:19:17 PM Permalink

i think you have a nice blog here... today was my first time coming here.. i just happened to find it doing a google search. anyway, good post.. i'll be bookmarking this blog for sure.

i know i'm a little off topic, but i just wanted to say i love the layout of your blog. i'm new to the blogegine platform, so any advice on getting my blog looking better would be appreciated.

learn to play the guitar United States
3/26/2010 10:37:09 AM Permalink

i know i'm a little off topic, but i just wanted to say i like the layout of your blog. i'm new to the blogegine platform, so any suggestions on getting my blog looking good would be appreciated.

Youtube Player Download United Kingdom
3/26/2010 11:36:06 AM Permalink

Great post. How long have you been running this blog for? It makes me realise that I need to improve mine a bit! Youtube Player Download

wow leveling guide United States
4/2/2010 4:36:36 PM Permalink

Pretty good post. I just stumbled upon your blog and wanted to say that I have really enjoyed reading your blog posts. Any way I'll be subscribing to your feed and I hope you post again soon.

link building website United States
4/4/2010 8:06:02 AM Permalink

This is good stuff. I am always looking for such great content to forward to my readers.

11 x 17 Scanner United States
4/4/2010 8:22:58 AM Permalink

i think that that was actually interesting. Good put up!¡­

Madlyn Harnett United States
4/4/2010 3:08:56 PM Permalink

Finally there`s a toolkit available in the market developed especially for unlocking game consoles. It enables you to open up everything from nes to playstation 3, really nice little thing from Shinobii called, not suprisingly, Console Toolkit.

Retirement Income United States
4/5/2010 5:08:52 AM Permalink

I guess there is always an easier way ...

Hausfinanzierung Rechner United States
4/5/2010 3:15:15 PM Permalink

There is obviously a lot to know about this. I think you  made lots of good points in your post.

horde leveling guide United States
4/6/2010 2:45:14 AM Permalink

Admiring the time and effort you put into your blog and detailed information you offer! I will bookmark your blog and have my children check up here often. Thumbs up!

Kitchen appliances United States
4/6/2010 7:24:35 AM Permalink

This is my first time I have visited here. I found a lot of interesting information in your blog. From the tons of comments on your posts, I guess I am not the only one! keep up the impressive work.

This was just what I was looking for. I'll bookmark this blog for sure!

While this subject can be very touchy for most people, my opinion is that there has to be a middle or common ground that we all can find. I do appreciate that youve added relevant and intelligent commentary here though. Thank you!

flavored coffee United States
4/7/2010 5:18:52 AM Permalink

You are absolutely correct on that!

clearance power tools United States
4/7/2010 7:49:57 AM Permalink

I can see that you are an expert at your field! I am launching a website soon, and your information will be very useful for me.. Thanks for all your help and wishing you all the success in your business.

Man Cerni United States
4/7/2010 11:38:39 AM Permalink

The iPhone is going to change medicine forever

Google Norway
4/9/2010 1:39:17 AM Permalink

Nice blog. Keep on writing

canon powershot s5 is United States
4/9/2010 3:15:38 PM Permalink

Thanks for taking this opportunity to talk about this, I feel strongly about it and I benefit from learning about this subject. If possible, as you gain data, please update this blog with new information. I have found it extremely useful.

home theatre sytems United States
4/10/2010 7:37:20 AM Permalink

This is my first time I have visited here. I found a lot of interesting information in your blog. From the volume of comments on your articles, I guess I am not the only one! keep up the impressive work.

Nice blog but I had some problems watching it in FFox. know why?

Early Learning Centre United States
4/10/2010 6:04:42 PM Permalink

Wow!, this was a real quality post. In theory I'd like to write like this too - taking time and real effort to make a good article... but what can I say... I keep putting it off and never seem to get something done

livejasmine United States
4/10/2010 7:33:21 PM Permalink

Useful information, many thanks to the author. It is puzzling to me now, but in general, the usefulness and importance is overwhelming. Very much thanks again and good luck!

rid of bed bugs United States
4/10/2010 8:30:23 PM Permalink

I am writing a paper on this issue and your article is very helpful and educational. Thank you.

Home and Gardens United States
4/11/2010 2:47:52 AM Permalink

I wanted to thank you for this interesting I definitely loved every little bit of it. I have you bookmarked your site to look at the latest stuff you post.