Welcome to MSDN Blogs Sign in | Join | Help

A New Chapter Begins

Originally I thought of this as my second departure from Microsoft, but when I consider the multiple stints I have spent as a contractor at Microsoft, I realize this is actually my fifth time to leave the company in one capacity or another.

Amazing.

My work in the project management software industry is far from complete. As many of you who have read this blog may recognize, I do the work I do because I love software and the positive impact our work has on people everywhere.

I want to say thank you for the many, many kind emails and phone calls I have received over the past two days.   I may be leaving, but I got your back and I will continue to be a resource for anybody who wants to understand how to build solutions with Microsoft’s EPM stack.  Many of the tools and applications I have been working on will not likely see the light of day in the immediate future, but ideas continue to swarm and I will address those new ideas in a different venue.  Indeed.. for I have a new job that I will talk about later!

My blog has yet to find a new home, but I am sure if you do a search in the near future for Project Server, the PSI, and Microsoft Project, you will find it in short order. 

This blog is hereby retired.

Colby

Posted by coafrica | 1 Comments

Breaking The Silence

It has been a while.  In addition to my daily project work, I am working on several side projects in various states of readiness:

1.)  Project Pro add-in for managing security

2.)  Project-Pro adding for managing custom fields

3.)  Schedule analyzer

4.)  Lookup table value import tool

5.)  Algorithms for overcoming the 1000-row project queue limitation

6.)  Project Server configuration documentation generator

7.)  Project Conference 2009 400-level track with Stephen Sanderlin from MSProjectExperts

I have been travelling quite a bit these days, so I am trying to spend more time with my family when I am home, so don’t expect these projects to come rolling off the assembly line any time soon.

Also, all of these projects are based on mpFx, so that project continues to evolve as well.

Let’s take a closer look at the security manager add-in.

Security Manager Add-In

Here is a screen shot:

image

I plan to implement CRUD operations on all security entities:

1.)  Users

2.)  Groups

3.)  Global Permissions (only allowing deleting or updating custom permissions)

4.)  Categories

5.)  Templates

I have extended mpFx’s security features to include the following methods:

 

image

NOTE: Many of these new methods are not supported because they are only minimally if at all covered in the SDK.

One area that is covered in the SDK is the creation of both global and category permissions.  The security add-in provides user interfaces for this:

image

And you can see this is now present in the list of project permissions:

image

And here it is in PWA:

image

Same for category permissions:

 

image

And again, here is the new category permission in PWA:

 

image

Another feature I have added is the ability to create a user:

image

Lookup Table Value Importer

The lookup table value importer that takes an Excel workbook as a data source, provides a means for mapping worksheets and columns to specific lookup tables, and then performs the import automatically.

Version 1.0 does not support hierarchical lookup tables. I am working on a user interface for this.

In addition, the tool validates the source data.  It provides spellchecking functionality, checks for illegal characters, looks for duplicates, inspects for fitness given the underlying data type (a date can’t be stored in a number list), and trims the input values to rid the data of the dreaded leading and trailing whitespace.

Here is a quick overview…

Logon

The tool reads your profile information from the registry, so if you have Project Pro installed, your profiles will appear in the drop down.  You can also create new profiles (which will not show up in Pro).  The tool supports logging in under alternate credentials, which I thought would be handy when you are onsite and trying to update lookup tables on a PS instance on the customer’s domain.

clip_image002

Working With Source Workbooks

Clicking the clip_image004 button produces a file open dialog.  Browse to the Excel workbook you would like to use as the source.  The tool scans each worksheet for columns and populates the user interface with  mapping elements that you use to point the tool to the target lookup table:

clip_image006

Note that the tabs  correspond to each worksheet found in the workbook.   Here is a screenshot of the underlying workbook so you can see how this works:

clip_image008

Clicking the clip_image010 button on each row in the grid displays the Select Lookup Table Dialog:

clip_image012

Choose the data type for the import.  Do this for each worksheet\column that you want to import:

clip_image014

Validation

After you have completed the mapping, clicking the clip_image016 button will open the workbook and perform validation on it.  A validator window is displayed above the workbook (it is part of the tool—I am using native calls to set its parent window to Excel):

clip_image018

Note that in addition to checking for duplicates and illegal characters, it looks at each value to make sure it fits the underlying data type.  Clicking the error in the validator box navigates you to the error in the workbook.

Clicking the save button saves the wordbook.  Clicking refresh causes the validation to run again.  Clicking the close button closes the workbook and returns you to the tool.

Importing

Click clip_image020 button to import:

clip_image022

Note on the status bar the red button, which allows you to cancel.

Other Features

A minor feature is the ability to indicate to the tool that the first row is a column header.

A more important feature is the ability to persist the map so you can reopen the workbook later and use the map without recreating it all over again.  The map is persisted in a hidden worksheet in the source workbook.

------------

Kind of neat.

Have a great week.

Posted by coafrica | 1 Comments
Filed under: , ,

TimedExecution

I haven’t written for a while. I have been super busy.  Chet said he signed up for my RSS feed today so I thought I would write a quick post so he has something to read today.  Hehe.

I am writing a tool that publishes projects on a schedule.  I want to do a Project Pro publish versus a publish with the PSI.   This tool runs unattended on the server.  Doing server-side automation of a singleton like Project Pro has its pitfalls—one is that since it is a singleton, if it gets hung up you can’t do much about it unless you code to defend yourself against such a case.

Enter the TimedExecution class. 

The concept is simple.  I want to run an operation and if it takes longer than a specified amount of time, I want to signal back to the caller that the operation is still executing.  The caller is then responsible for figuring out how to react.

Here is the source:

using System;
using System.Threading;
using Mcs.Epm.ProjectPro.Properties;
 
namespace Mcs.Epm.ProjectPro
{
    internal class TimedExecution
    {
        private readonly Action<string> _Action;
        private readonly string _State;
        private readonly int _RunTimeInMilliseconds;
 
        public TimedExecution(Action<string> action, string state, int runTimeInMilliseconds)
        {
            _Action = action;
            _State = state;
            _RunTimeInMilliseconds = runTimeInMilliseconds;
        }
 
        public bool Execute()
        {
            Thread executeThread = new Thread(ExecuteThreadEntry);
            executeThread.Name = Resources.AppTitle;
 
            executeThread.Start();
 
            executeThread.Join(_RunTimeInMilliseconds);
 
            return !executeThread.IsAlive;
        }
 
        private void ExecuteThreadEntry()
        {
            _Action.Invoke(_State);
        }
    }
}

Pretty straightforward.  I use the Action delegate to pass TimedExecution the method in the caller that will be called, as well as state information (in this case, the name of the project to publish), and the max run time in milliseconds.   I create a new thread and invoke the action.  The primary thread joins on the new thread for the specified time.   Once Join returns, Execute returns true if the thread is no longer alive.

Here is its usage:

TimedExecution timedExecution = new TimedExecution(PublishProject, project.Name, _AutomationOptions.MaxPublishRunTime * 1000);
 
if (!timedExecution.Execute())
{
    _ProjectWrapper.Dispose();
    OpenProjectWrapper();
}

This is the brute force stuff you have to do when automating Project Pro on the server.   In order to be as friendly and as considerate as possible, I implemented a whole slew of options for controlling how Project Pro is automated:

image

Have a great weekend!

Posted by coafrica | 1 Comments

Project Server Interface Development: Dev 101 Samples

I did a presentation today about basic PSI development concepts.   I thought I would share these examples with the world, so here you go!

image

http://code.msdn.microsoft.com/Project/Download/FileDownload.aspx?ProjectName=PsiDev101&DownloadId=5141

Colby

Posted by coafrica | 1 Comments

Learning SharePoint Part VII – List Pagination

A very small group of you saw this post go up and then come right back down again.  I found a bug about 30 seconds after posting this.    Here is the updated post.  NOTE: The bug had to do with not fully understanding how the pagination handled moving backwards in the list.  It appeared to be doing it correctly, but when I started to look closely at the actual items, it turned out that it was ALWAYS MOVING FORWARD.  The key is that I missed the PagedPrev query string element, which I now am including correctly.  The code has been updated below.  Beware of of voodoo programming.  :-)


It seems like everything takes just a little longer to learn than it should with SharePoint.  I am building a webpart that browses list items and allows the user to perform special actions on the list item.  After creating item styles and coming up with a CSS container to match the repeating pattern I needed, I went to add pagination to the item browser.   Pagination is usually represented in the user interface by little left-right arrows or “next page”/”previous page”.  This should be easy and it is, once you understand how SharePoint does pagination.

SPListItemCollectionPosition

The key to SharePoint list pagination is the SPListItemCollectionPosition class. The documentation’s one example didn’t help me with my specific requirements.  In particular, I need to page through the list data sorted by the “Created” date field.

The SPListItemCollectionPosition’s constructor takes one string parameter called PagingInfo.  The SDK’s description of this parameter is particularly useless: Gets or sets paging information used to generate the next page of data.  What about the string’s structure and contents?  How do you use it?   Another friendly MSDN blogger, Deepak Badki, had part of the answer here.

But rather than just copy his code, I needed to understand it first. 

If you setup a list view such that it is sorted and the number of items to display is less than the number of items, you can examine the query strings resulting from paging from one page to the next and back.  Here are some examples:

First Page

AllItems.aspx

Next Page

AllItems.aspx?Paged=TRUE&p_Created=20090217%2018%3a22%3a14&p_ID=3&View=%7bB32E95A4%2d5675%2d42A5%2d8C13%2d569D13BDCAF9%7d&PageFirstRow=4

Next Page

AllItems.aspx?Paged=TRUE&p_Created=20090217%2018%3a22%3a34&p_ID=6&View=%7bB32E95A4%2d5675%2d42A5%2d8C13%2d569D13BDCAF9%7d&PageFirstRow=7

Previous Page

AllItems.aspx?Paged=TRUE&PagedPrev=TRUE&p_Created=20090217%2020%3a51%3a20&p_ID=7&View=%7bB32E95A4%2d5675%2d42A5%2d8C13%2d569D13BDCAF9%7d&PageLastRow=6

In this particular instance, the list view is configured to sort by the created date.  Let’s break down the the query string:

AllItems.aspx? – The list page

Paged=TRUE – Indicates that the list is paged.

PagedPrev=TRUE – This element only appears when the previous page is visited.

p_Created= – The first sort by parameter, which is “Created” prefixed with “p_”.  The right hand is the encoded universal date and time.

p_ID= – This is the ID of the previous page’s last item’s ID.  This is important.

View= – This is the encoded GUID for the current list. 

PageFirstRow= – The ID of the current page’s first item. 

Newbie Webpart Developer

I am definitely new to webpart development.  I have done some work in ASP.NET but not for a while.  I know that sounds strange given that so much of what we do involves web development, but I have been a back-end guy for a long, long time.  So, this is really exciting for me and a bit painful.  ASP.NET’s attempt to be like WinForms in many ways offers tremendous advantage, but it also is a bit like taking a boat on land.  I am also exploring the MVC pattern, which seems like a better fit for web development.  That is neither here nor there because to build a webpart, you have to think like a server control developer—something I haven’t done since the ASP.NET 1.0 era.

So, my challenge is to learn the SharePoint way and also learn ASP.NET, and learn how to make things pretty.  I had help on the last bit from the product group.  Here is a screenshot of the basic item browser (the browser is a browser of ideas, a topic for another post):

image 

The browser webpart is configured to browse three items at a time (this is a personalization aspect of the webpart).   At the core of the browser is one method:

   1: public static SPListItemCollection GetListPage(SPList list,
   2:                                                uint pageSize,
   3:                                                int pageIndex,                                                        
   4:                                                string pagingInfo,
   5:                                                string fields,
   6:                                                string queryCaml,
   7:                                                out bool isEndOfList)
   8: {
   9:     
  10:     SPQuery query = new SPQuery();
  11:  
  12:     query.RowLimit = pageSize;
  13:     query.ViewFields = fields;
  14:     query.Query = queryCaml;
  15:  
  16:     if (!string.IsNullOrEmpty(pagingInfo))
  17:     {
  18:         SPListItemCollectionPosition collectionPosition = new SPListItemCollectionPosition(pagingInfo);                
  19:         query.ListItemCollectionPosition = collectionPosition;
  20:     }
  21:  
  22:     SPListItemCollection returnValue = list.GetItems(query);
  23:  
  24:     isEndOfList = (((pageIndex - 1) * pageSize) + returnValue.Count) >= list.ItemCount;
  25:  
  26:     return returnValue;
  27: }

First, I really dislike methods that take this many parameters, but it takes a lot to feed this little algorithm:

SPList list – The list
uint pageSize – The page size (number of ideas per page)
int pageIndex – The current page index                                                       
string pagingInfo – The paging info (more on this later)
string fields – The fields to bring back from the list (in CAML)
string queryCaml – The CAML query to execute
out bool isEndOfList – An out parameter which signals that the results are the end of the list

Here is the method in context:

   1: SPList list = web.Lists.GetList(listGuid, false);
   2: SPView view = list.GetView(viewGuid);
   3:  
   4: List<string> fields = new List<string>();
   5:  
   6: foreach (string field in view.ViewFields)
   7: {
   8:     fields.Add(field);
   9: }
  10:  
  11: SPListItemCollection pageItems = Fx.GetListPage(list,
  12:                                                 (uint) IdeasPerPage,
  13:                                                 CurrentPage,
  14:                                                 PageInfo,                                                            
  15:                                                 Fx.GetFieldRefCaml(fields.ToArray()),
  16:                                                 caml,
  17:                                                 out _EndOfList);
  18: RenderIdeas(pageItems);

I get the list from the web and the default view.  I pack up the fields and send the whole shebang to GetListPage.  So, where does the CurrentPage and the PageInfo come from?

The key is to remember the current page’s first and last item and derive the PageInfo from that data.  Lets walk through this:

1.)  RenderIdeas – Capture the first and last ideas and send them along to AddNavigation

2.) AddNavigation – Use the first and last items, the list view GUID, and the current state to setup the navigation elements used for paging.

Okay, now for the code:

   1: private void RenderIdeas(SPListItemCollection pageItems, Guid viewGuid)
   2: {
   3:     if (CurrentPage == 1 && pageItems.Count == 0)
   4:     {
   5:         CreateNoIdeasWarning();
   6:  
   7:         return;
   8:     }
   9:  
  10:     SPListItem lastIdea = null;
  11:  
  12:     foreach (SPListItem idea in pageItems)
  13:     {
  14:         Panel ideaContainer = CreateIdeaContainer(idea);
  15:  
  16:         _ContainerPanel.Controls.Add(ideaContainer);
  17:  
  18:         lastIdea = idea;
  19:     }
  20:  
  21:     AddNavigation(pageItems[0], lastIdea, viewGuid);
  22: }
 
Pretty straightforward.  If the list is empty, show a warning; otherwise, render each idea in the list returned by GetListPage.   Now, for AddNavigation:
 
   1: private void AddNavigation(SPListItem firstIdea, SPListItem lastIdea, Guid viewGuid)
   2: {            
   3:     DateTime lastItemCreatedDate = (DateTime) lastIdea["Created"];
   4:     int lastItemId = (int) lastIdea["ID"];
   5:  
   6:     string lastItemDateData = lastItemCreatedDate.ToUniversalTime().ToString("yyyyMMdd hh:mm:ss");
   7:  
   8:     string nextPage = string.Format("Paged=TRUE~p_{0}={1}~p_ID={2}~View={3}~PageFirstRow={4}",
   9:                                     "Created",
  10:                                     SPEncode.UrlEncode(lastItemDateData),
  11:                                     lastItemId,
  12:                                     SPEncode.UrlEncode(viewGuid.ToString()),
  13:                                     firstIdea["ID"]);
  14:  
  15:     string previousPage;
  16:  
  17:     if (CurrentPage > 2)
  18:     {
  19:         previousPage = string.Format("Paged=TRUE~PagedPrev=TRUE~p_{0}={1}~p_ID={2}~View={3}~PageFirstRow={4}",
  20:                                      "Created",
  21:                                      SPEncode.UrlEncode(lastItemDateData),
  22:                                      lastItemId,
  23:                                      SPEncode.UrlEncode(viewGuid.ToString()),
  24:                                      firstIdea["ID"]);
  25:     }
  26:     else
  27:     {
  28:         previousPage = string.Format("Paged=TRUE~View={0}", SPEncode.UrlEncode(viewGuid.ToString()));
  29:     }
  30:  
  31:  
  32:     HyperLink previousPageLink = null;
  33:     if (CurrentPage > 1)
  34:     {
  35:         previousPageLink = new HyperLink();
  36:         previousPageLink.Text = "Previous Page";
  37:         previousPageLink.CssClass = "IdeaLeftArrow";
  38:         previousPageLink.NavigateUrl = string.Format("{0}?Page={1}&PageInfo={2}", Page.Request.Url.GetLeftPart(UriPartial.Path), CurrentPage - 1, previousPage);
  39:     }
  40:  
  41:     HyperLink nextPageLink = null;
  42:  
  43:     if (!_EndOfList)
  44:     {
  45:         nextPageLink = new HyperLink();
  46:         nextPageLink.Text = "Next Page";
  47:         nextPageLink.CssClass = "IdeaRightArrow";
  48:         nextPageLink.NavigateUrl = string.Format("{0}?Page={1}&PageInfo={2}", Page.Request.Url.GetLeftPart(UriPartial.Path), CurrentPage + 1, nextPage);
  49:     }
  50:  
  51:     if (nextPageLink == null && previousPageLink == null)
  52:     {
  53:         return;
  54:     }
  55:  
  56:     Panel navigationPanel = new Panel();
  57:  
  58:     navigationPanel.CssClass = "IdeaNavigationPanel";
  59:  
  60:     if (previousPageLink != null)
  61:     {
  62:         navigationPanel.Controls.Add(previousPageLink);
  63:     }
  64:     if (nextPageLink != null)
  65:     {
  66:         navigationPanel.Controls.Add(nextPageLink);
  67:     }
  68:  
  69:     _ContainerPanel.Controls.Add(navigationPanel);
  70: }
 
This should look familiar because I am constructing two query strings similar to what was discussed in the beginning of the post.  The next page URL is pretty straightforward.  The previous page URL requires a bit more explanation.  If CurrentPage is great than 2, I have to insert the PagedPrev element, otherwise it is sufficient to just add the list GUID element.

When the user clicks one of the links, this paging information is parsed out of the query string and feeds the next pagination call (from the OnLoad event):

   1: if (Page.Request.QueryString.HasKeys())
   2: {
   3:     if (Page.Request.QueryString["Page"] != null)
   4:     {
   5:         int value;
   6:         if (int.TryParse(Page.Request.QueryString["Page"], out value))
   7:         {
   8:             CurrentPage = value;
   9:         }
  10:     }
  11:  
  12:     if (Page.Request.QueryString["PageInfo"] != null)
  13:     {
  14:         _PageInfo = Page.Request.QueryString["PageInfo"];
  15:         _PageInfo = _PageInfo.Replace("~", "&");
  16:     }
  17: }
  18:  
  19: EnsureChildControls();
  20: RenderIdeaList();

That’s it folks.  I hope this helps somebody because this took me about 5 hours to figure it out.

Posted by coafrica | 2 Comments
Filed under:

MSDN Code Gallery Report Released

Here it is.  Download the setup here.  Download the source here.

Enjoy.

This will be my last for a while.  I have to focus on the book and I am off to a 20 week engagement.

Posted by coafrica | 1 Comments
Filed under:

MSDN Code Gallery Download Report

In an attempt to divert my brain and as a break from SharePoint development, I wrote  a simple utility this weekend that generates a consolidated report of download counts for any resource housed at MSDN Code Gallery, including projects, releases, and files.  Here is a screenshot:

image

The user interface takes a resource (or project) name and adds it to a list of projects for which the download count report is generated.  Using a rich text control, the tool generates a simple report of the downloads for each element.

Here is a class diagram which illustrates the basics:

image

I used the Html Agility Pack for scraping the MSDN Code Gallery pages, which is what the tool relies on for generating the reports.  Here is a simple example of how to use the library:

   1: GalleryResources galleryResources = new GalleryResources(true);
   2:  
   3: if (galleryResources.IsValidResource("notarealresource"))
   4: {
   5:     galleryResources.RegisterResource("mschart");
   6: }
   7:  
   8: if (galleryResources.IsValidResource("mschart"))
   9: {
  10:     galleryResources.RegisterResource("mschart");    
  11: }
  12:  
  13: if (galleryResources.IsValidResource("mpFx"))
  14: {
  15:     galleryResources.RegisterResource("mpFx");
  16: }
  17:  
  18: foreach (GalleryResource galleryResource in galleryResources)
  19: {
  20:     Debug.WriteLine(galleryResource.Name);
  21:     foreach (Release release in galleryResource)
  22:     {
  23:         Debug.WriteLine(release.Name);
  24:         foreach (ReleaseFile file in release)
  25:         {
  26:             Debug.WriteLine(file.Name);
  27:             Debug.WriteLine(file.DownloadCount);
  28:         }
  29:     }
  30: }
  31:  
  32: galleryResources.Save();

The resources are persisted to a local store.  The boolean handed to the constructor simply instructs the constructors to attempt to load the currently registered resources from a file.  The call to Save on line 32 saves the registered resources back out to that file.

IsValidResource attempts to load the resource from Code Gallery.  It detects whether the resource exists by a simple algorithm which uses the HTML header tags to determine if the request was routed to the Code Gallery home page, which indicates the resource doesn’t exist:

   1: public bool IsValidResource(string name)
   2: {
   3:     try
   4:     {
   5:         HtmlWeb web = new HtmlWeb();
   6:  
   7:         string url = string.Format(Settings.Default.CodeGalleryResource, name);
   8:  
   9:         HtmlDocument document = web.Load(url);
  10:  
  11:         HtmlNodeCollection headers = document.DocumentNode.SelectNodes("//h1");
  12:  
  13:         foreach (HtmlNode node in headers)
  14:         {
  15:             if (node.InnerText.Equals(Settings.Default.CodeGallery))
  16:             {
  17:                 return false;
  18:             }
  19:         }
  20:  
  21:         return true;
  22:     }
  23:     catch (Exception exception)
  24:     {
  25:         Trace.WriteLine(exception.Message);
  26:     }
  27:  
  28:     return false;
  29: }

RegisterResource simply adds the resource to the list of resources for which download count reports will be generated:

   1: public void RegisterResource(string name)
   2: {
   3:     if (ContainsResource(name))
   4:     {
   5:         return;
   6:     }
   7:  
   8:     GalleryResource galleryResource = new GalleryResource(name);
   9:  
  10:     _GalleryResources.Add(galleryResource);
  11:  
  12: }
  13:  

The foreach loop is where the action happens.  Using Code Gallery’s RSS feeds:

image

I consume the Releases feed:

   1: private void LoadReleases()
   2: {
   3:     try
   4:     {
   5:         Releases = new List<Release>();
   6:  
   7:         using (XmlReader reader = XmlReader.Create(string.Format(Settings.Default.CodeGalleryRssFeed, Name)))
   8:         {
   9:             SyndicationFeed feed = SyndicationFeed.Load(reader);
  10:  
  11:             if (feed != null)
  12:             {
  13:                 foreach (SyndicationItem item in feed.Items)
  14:                 {
  15:                     string itemTitle = item.Title.Text;
  16:                     itemTitle = itemTitle.Replace("RELEASED:", string.Empty);
  17:                     itemTitle = itemTitle.Replace("CREATED RELEASE:", string.Empty);
  18:                     itemTitle = itemTitle.Replace("UPDATED RELEASE:", string.Empty);
  19:  
  20:                     bool found = false;
  21:  
  22:                     foreach (Release existingRelease in Releases)
  23:                     {
  24:                         if (existingRelease.Name.Equals(itemTitle))
  25:                         {
  26:                             found = true;
  27:                             break;
  28:                         }
  29:                     }
  30:  
  31:                     if (!found)
  32:                     {
  33:                         Debug.Assert(item.Links.Count == 1);
  34:                         Release release = new Release(itemTitle, item.Links[0].Uri.ToString());
  35:                         Releases.Add(release);
  36:                     }
  37:                 }
  38:             }
  39:         }
  40:     }
  41:     catch (Exception exception)
  42:     {
  43:         Trace.Write(exception.Message);
  44:     }
  45:  
  46: }
  47:  

I check for duplicates, stripping out the different variations that the feed returns.  When I find a unique release that doesn’t exist in the collection, I add it to the list of releases.  You see that on line 34.  When you access the Release object’s release files the lazy load does the work to scrape the screen:

   1: private void LoadReleaseFiles()
   2: {
   3:     Files = new List<ReleaseFile>();
   4:  
   5:     HtmlWeb htmlWeb = new HtmlWeb();
   6:     HtmlDocument htmlDocument = htmlWeb.Load(Link);
   7:  
   8:     HtmlNodeCollection nodes = htmlDocument.DocumentNode.SelectNodes("//div[@class='FileListItemDiv']");
   9:  
  10:     foreach (HtmlNode node in nodes)
  11:     {
  12:         string innerText = node.InnerText;
  13:  
  14:         innerText = innerText.Replace("\r", "");
  15:  
  16:         string[] components = innerText.Split('\n');
  17:  
  18:         Debug.Assert(components.Length == NUM_FILE_INFO_COMPONENTS);
  19:  
  20:         string releaseName = components[FILE_INFO_NAME].Trim();
  21:  
  22:         releaseName = HttpUtility.HtmlDecode(releaseName);
  23:  
  24:         string releaseTemp = components[FILE_INFO_DOWNLOADS].Replace("downloads", "").Trim();
  25:  
  26:         int releaseDownloadCount;
  27:  
  28:         if (!int.TryParse(releaseTemp, out releaseDownloadCount))
  29:         {
  30:             releaseDownloadCount = -1;
  31:         }
  32:  
  33:         ReleaseFile releaseFile = new ReleaseFile(releaseName, releaseDownloadCount);
  34:  
  35:         Files.Add(releaseFile);
  36:     }
  37: }

I have to do some guess-work here because I don’t own these pages, but basically I determined which div contained the release’s file information.  I do some parsing, figure out which component of the markup contains the file name and download count and add a new ReleaseFile to the collection.

That’s pretty much it.  I will get the source posted to Code Gallery tomorrow evening.

Posted by coafrica | 1 Comments
Filed under:

MpFx Walkthrough: Creating the ProjectServer Object & Enumerating Project Information

I am taking a little break from my current project.  Whew. Only such much you can cram into your brain about SharePoint in one unbroken period of time.

Today’s topic starts at the beginning of mpFx:  How to create a ProjectServer object.  I will also demonstrate enumerating project information.  The code sample and an update to the core mpFx library are available on Code Gallery.  Here is the updated mpFx core library and here is the walkthrough.

The following references are required to build and execute:

  • Mcs.Epm.MicrosoftProject.mpFx
  • System
  • System.Core
  • System.Data
  • System.Xml

NOTE: mpFx 1.0 PREVIEW is intended to be a learning aide.  There are problems with parts of the code related to resource acquisition and clean up, performance, and generally some design decisions I made well before I had a full understanding of what the PSI was up to.  I am slowly purging those areas that need purging…

NOTE II: Parts of the PREVIEW edition implement IDisposable where it isn’t really a benefit.  DataSets implement IDispose as part of MarshalByValueComponent, but the source of IDisposable in this case is IComponent!    It is recommended that you call Dispose on any object the implements it, but in this case I can’t see any reason to.  No resources are freed as part of the call, as far as I can tell.

Okay, on to the sample!

Walkthrough

Let’s begin with the sample:

   1: using System;
   2: using System.IO;
   3: using Mcs.Epm.MicrosoftProject.mpFx;
   4: using Mcs.Epm.MicrosoftProject.mpFx.ProjectsWebService;
   5:  
   6: namespace CreateProjectServerObject
   7: {
   8:     class Program
   9:     {
  10:         static void Main()
  11:         {
  12:             /*  There are four constructors for the ProjectServer object.  This demo describes 
  13:              *  the constructor most commonly used in my projects: 
  14:              *  
  15:              *  ProjectServer(string projectServerUrl, DataStoreEnum store, ILog log)
  16:              *  
  17:              *  The constructor takes three parameters:
  18:              *  
  19:              *  1.) projectServerUrl: The full path to the Project Server PWA site
  20:              *  2.) DataStorEnum: The store on which operations will be performed
  21:              *  3.) ILog: An object implementing the ILog interface, which 
  22:              *      performs logging operations.
  23:              *      
  24:              *  At the time of writing, a single class implements ILog and implements
  25:              *  logging to the file system.  Other classes might implement ILog and 
  26:              *  persist events to the Event Log or a database.            
  27:              */
  28:  
  29:             string logDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "MpFx");
  30:  
  31:             try
  32:             {
  33:                 using (Log log = new Log(logDirectory, "Demo", LogPeriod.Hourly, true))
  34:                 using (ProjectServer projectServer = new ProjectServer("http://projectserver/pwa", DataStoreEnum.WorkingStore, log))
  35:                 {
  36:                     ProjectCollection projects = projectServer.Projects;
  37:  
  38:                     foreach (EnterpriseProject project in projects)
  39:                     {
  40:                         string projectInformation = string.Format("Project Name: {0} - Start Date: {1}",
  41:                                                                   project.Name,
  42:                                                                   EnterpriseProject.StandardInfo(project).PROJ_INFO_START_DATE);
  43:                                                 
  44:                         Console.WriteLine(projectInformation);                     
  45:                     }
  46:                 }
  47:             }
  48:             catch (MpFxException exception)
  49:             {
  50:                 Console.Write(Errors.ProcessMpFxException(exception));
  51:             }
  52:             catch (Exception exception)
  53:             {
  54:                 Console.Write(exception.Message);
  55:             }
  56:  
  57:             Console.WriteLine("Projects enumerated  Press any key to close.");
  58:             Console.ReadKey();
  59:         }
  60:     }
  61: }

We will skip the details on logging for this post.  To begin, let’s look at one of the four constructors implemented on ProjectServer (I chose the one I most commonly use).  In the sample above, the constructor is called on line 34.  Here is the constructor source:

   1: public ProjectServer(string projectServerUrl, DataStoreEnum store, ILog log)
   2: {            
   3:     try
   4:     {
   5:         Log = log;
   6:  
   7:         if (!Utilities.IsValidUrl(projectServerUrl))
   8:         {
   9:             throw new ArgumentException(LibraryResources.InvalidProjectServerUrl);
  10:         }
  11:  
  12:         Settings = new ProjectServerSettings();
  13:         WebServices = new WebServices(this);
  14:         Site = new Uri(projectServerUrl);
  15:         Store = store;
  16:  
  17:         NetworkCredential = CredentialCache.DefaultNetworkCredentials;
  18:         AuthenticationType = AuthenticationType.Windows;
  19:  
  20:         WebServices.LoginWindows = new LoginWindows();
  21:  
  22:         WebServices.LoginWindows.Url = WebServices.AppendPath(projectServerUrl, ServicePaths.WindowsLoginService);
  23:         WebServices.LoginWindows.UseDefaultCredentials = true;
  24:  
  25:         WriteLogEntry(LogArea.Constructor, 
  26:                       LogEntryType.Information, 
  27:                       string.Format(LibraryResources.LogConstructor, AuthenticationType, projectServerUrl));
  28:  
  29:         WebServices.LoginWindows.Login();
  30:  
  31:         Settings.ListSeparator = WebServices.Projects.ReadServerListSeparator();
  32:  
  33:     }
  34:     catch (SoapException exception)
  35:     {
  36:         throw MpFxException.Create(exception, Log, LogArea.Constructor, LogEntryType.Error);
  37:     }
  38:     catch (ArgumentException exception)
  39:     {
  40:         throw MpFxException.Create(exception, Log, LogArea.Constructor, LogEntryType.Error);
  41:     }
  42:     catch (Exception exception)
  43:     {
  44:         throw MpFxException.Create(exception, Log, LogArea.Constructor, LogEntryType.Error);
  45:     }
  46: }

Here are a few elaborations and considerations:

  • Lines 12-15:  mpFx attempts to build an object model from the disparate web services and data of the PSI.  From the ProjectServer object, you can directly access the underlying web services, by accessing the WebServices property (projectServer.WebServices.[webServiceName]) or you can access the encapsulation objects and use the helper methods I have created.  For example, to create a project you can populate a ProjectDataSet object, directly access the project web service, call QueueCreateProject, wait on the job, and deal with the potential exceptions yourself, or you can call projectServer.Projects.Create, which looks like this:
   1: /// <summary>
   2: /// Create a project in Project Server.
   3: /// </summary>
   4: /// <param name="project">Project</param>
   5: /// <param name="validateOnly">Indicates whether the operation should validate only or perform the creation</param>
   6: /// <param name="wait">Indicates whether the call should wait on the queued job.</param>
   7: /// <returns>Queue job GUID</returns>
   8: public Guid Create(ProjectDataSet project, bool validateOnly, bool wait)
   9: {
  10:    try
  11:    {
  12:        Guid jobGuid = Guid.NewGuid();
  13:  
  14:        Parent.WebServices.Projects.QueueCreateProject(jobGuid, project, validateOnly);                
  15:  
  16:        if (wait)
  17:        {
  18:            string errorMessage;
  19:  
  20:            Parent.Queue.WaitOnJobStatus(jobGuid,
  21:                                         JobState.Success,
  22:                                         Parent.Settings.QueueStatusRetryCount,
  23:                                         Parent.Settings.QueueStatusSleepDuration,
  24:                                         out errorMessage);
  25:  
  26:            MpFxException.ThrowIfError(errorMessage, Parent.Log, LogArea.CreateProject, LogEntryType.Error);                    
  27:            
  28:        }
  29:  
  30:        return jobGuid;
  31:  
  32:    }
  33:    catch (SoapException exception)
  34:    {
  35:        throw MpFxException.Create(exception, Parent.Log, LogArea.CreateProject, LogEntryType.Error);
  36:    }
  37:    catch (Exception exception)
  38:    {
  39:        throw MpFxException.Create(exception, Parent.Log, LogArea.CreateProject, LogEntryType.Error);
  40:    }
  41: }

  • The particular constructor we used in the sample uses Windows authentication when communicating with the PSI.  NOTE: Forms authentication has NOT been tested with mpFx 1.0 PREVIEW

Returning to the sample:

  36:                     ProjectCollection projects = projectServer.Projects;

The Projects property returns a ProjectCollection object. Looking at the property implementation uncovers another design decision:

   1: public ProjectCollection Projects
   2: {
   3:     get
   4:     {
   5:         if (_ProjectCollection == null)
   6:         {
   7:             _ProjectCollection = new ProjectCollection(this);
   8:         }
   9:  
  10:         return _ProjectCollection;
  11:     }
  12: }

The lazy load pattern is widely employed (with various degrees of success and cleanliness!) throughout mpFx.   Examining the internals of the next line of code further illustrates this point:

  38:                     foreach (EnterpriseProject project in projects)

Let’s take a look at the enumerator implementation.  First, the ProjectCollection implements two IEnumerables:

   1: public class ProjectCollection : IEnumerable<EnterpriseProject>, IEnumerable<Guid>

The first enumerates EnterpriseProjects (we will get to this later) and the second enumerates Guids.  Let’s take a look at the EnterpriseProject enumerator, which is the one used in the sample:

   1: public IEnumerator<EnterpriseProject> GetEnumerator()
   2: {
   3:     return ((IEnumerable<EnterpriseProject>)this).GetEnumerator();
   4: }

And:

   1: IEnumerator<EnterpriseProject> IEnumerable<EnterpriseProject>.GetEnumerator()
   2: {
   3:     if (_projectsCollection == null)
   4:     {
   5:         LoadProjectCollection();
   6:     }
   7:  
   8:     ThrowLoadCollectionLoadException();
   9:  
  10:     foreach (KeyValuePair<Guid, EnterpriseProject> pair in _projectsCollection)
  11:     {
  12:         yield return pair.Value;
  13:     }
  14: }

Note that the backing field is first checked for nullness.  If it is null, the project collection is loaded:

   1: internal void LoadProjectCollection()
   2: {
   3:     if (_projectsCollection == null)
   4:     {
   5:         _projectsCollection = new Dictionary<Guid, EnterpriseProject>();
   6:     }
   7:     else
   8:     {
   9:         _projectsCollection.Clear();
  10:     }
  11:  
  12:     using (ProjectDataSet projectDataSet = Parent.WebServices.Projects.ReadProjectStatus(Guid.Empty, Parent.Store, string.Empty, (int)Project.ProjectType.Project))
  13:     {
  14:         foreach (ProjectDataSet.ProjectRow project in projectDataSet.Project.Rows)
  15:         {
  16:             if (project.PROJ_UID != Guid.Empty)
  17:             {
  18:                 _projectsCollection.Add(project.PROJ_UID, new EnterpriseProject(this, project.PROJ_UID, project.PROJ_NAME, Parent.Store));
  19:             }
  20:         }
  21:     }
  22: }

imagePretty straightforward so far.  The ProjectCollection contains EnterpriseProjects.  Rather than build a full model of all of the ProjectDataSet.ProjectRow members, it exposes data and helper methods.  See the class diagram to the left.

The properties and methods that are currently implement reflect the learning requirements I experienced while developing mpFx in the early days.  Essentially, every time I needed to read or act on project data in a new way, I would implement a property or method appropriate to the requirement. 

Returning to the sample:

  40:                         string projectInformation = string.Format("Project Name: {0} - Start Date: {1}",
  41:                                                                   project.Name,
  42:                                                                   EnterpriseProject.StandardInfo(project).PROJ_INFO_START_DATE);
  43:                                                 

The project name is one of a few properties that wrap elements of the ProjectDataSet.ProjectRow data row class.  The helper method EnterpriseProject.StandardInfo is short hand for project.StandardInformation.Project[0].  Accessing the StandardInformation property causes a call to be made to Project Server:

   1: public ProjectDataSet StandardInformation
   2: {
   3:     get
   4:     {
   5:         return Parent.Parent.WebServices.Projects.ReadProjectEntities(ProjectGuid, (int)ProjectEntityType.Project, Parent.Parent.Store);
   6:     }
   7: }

That’s pretty much it!

More later.  Back to SharePoint.

Posted by coafrica | 1 Comments
Filed under: , ,

mpFx: Simon Demo

Simon from Australia needed an example of how to use mpFx to create projects, resources, tasks, and assignments.  Here it is:

http://code.msdn.microsoft.com/Project/Download/FileDownload.aspx?ProjectName=mpFx&DownloadId=4636.

Here is the source:

   1: using System;
   2: using System.Web.Services.Protocols;
   3: using Mcs.Epm.MicrosoftProject.mpFx;
   4: using Mcs.Epm.MicrosoftProject.mpFx.ProjectsWebService;
   5: using Mcs.Epm.MicrosoftProject.mpFx.ResourcesWebService;
   6: using Microsoft.Office.Project.Server.Library;
   7: using DataStoreEnum=Mcs.Epm.MicrosoftProject.mpFx.ProjectsWebService.DataStoreEnum;
   8: using Resource = Microsoft.Office.Project.Server.Library.Resource;
   9:  
  10: namespace EntityCreationDemo
  11: {
  12:     class Program
  13:     {
  14:         static void Main(string[] args)
  15:         {
  16:             try
  17:             {
  18:                 using (ProjectServer projectServer = new ProjectServer("http://projectserver/pwa", DataStoreEnum.WorkingStore, null))
  19:                 {
  20:                     string errorMessage;
  21:  
  22:                     Guid projectGuid = Guid.NewGuid();
  23:                     
  24:                     //  Create the project
  25:                     using (ProjectDataSet projectDataSet = EntityFactory.NewProject("Simon Test", projectGuid))
  26:                     {
  27:                         projectServer.Projects.Create(projectDataSet, false, true);
  28:                     }
  29:  
  30:                     // This is actually a bug of omission.  I should be updating the underlying project collection on creation.  
  31:                     // In order to use the current tech preview of mpFx, we brute force to refresh the projects collection 
  32:                     projectServer.Projects.Refresh();
  33:  
  34:                     //  Grab the project object
  35:                     EnterpriseProject project = projectServer.Projects[projectGuid];
  36:  
  37:                     Guid taskGuid;
  38:  
  39:                     //  Create the task
  40:                     project.CreateTask("Test Task",
  41:                                        DateTime.Now,
  42:                                        1000,
  43:                                        true,
  44:                                        true,
  45:                                        Guid.NewGuid(),
  46:                                        out taskGuid,
  47:                                        out errorMessage);
  48:  
  49:                     if (IsPsClientError(errorMessage))
  50:                     {
  51:                         return;
  52:                     }
  53:  
  54:                     Guid resourceGuid = Guid.NewGuid();
  55:  
  56:                     //  Create the resource
  57:                     using (ResourceDataSet resourceDataSet = EntityFactory.NewResource("Simon", Resource.Type.WorkResource, resourceGuid))
  58:                     {
  59:                         projectServer.Resources.Create(resourceDataSet, false, true);
  60:                     }
  61:  
  62:                     Guid assignmentGuid = Guid.NewGuid();
  63:  
  64:                     //  Create the assignment
  65:                     project.CreateAssignment(assignmentGuid, taskGuid, resourceGuid, true, true, true, Guid.NewGuid(), out errorMessage);
  66:  
  67:                     if (IsPsClientError(errorMessage))
  68:                     {
  69:                         return;
  70:                     }
  71:                 }
  72:             }
  73:             catch (MpFxException exception)
  74:             {
  75:                 Console.Write(Errors.ProcessMpFxException(exception));
  76:             }
  77:             catch (SoapException exception)
  78:             {
  79:                 Console.Write(Errors.ProcessMSProjectErrors(exception));
  80:             }
  81:             catch (Exception exception)
  82:             {
  83:                 Console.Write(exception.Message);
  84:             }
  85:  
  86:             Console.ReadKey();
  87:         }
  88:  
  89:         /// <summary>
  90:         /// Check the string returned by waitable calls to see if it is a a PSClientError
  91:         /// </summary>
  92:         /// <param name="errorMessage">The error message xml</param>
  93:         /// <returns></returns>
  94:         private static bool IsPsClientError(string errorMessage)
  95:         {
  96:             if (string.IsNullOrEmpty(errorMessage))
  97:             {
  98:                 return false;
  99:             }
 100:  
 101:             PSClientError clientError = new PSClientError(errorMessage);
 102:  
 103:             if (clientError.Count == 0)
 104:             {
 105:                 return false;
 106:             }
 107:  
 108:             Console.Write(Errors.FormatProjectError(clientError.GetAllErrors(), errorMessage));
 109:  
 110:             return true;
 111:         }
 112:     }
 113: }
Posted by coafrica | 1 Comments
Filed under:

Learning SharePoint Part IV

I have learned so much in the past few days, I am not sure where to start.  So, here is tidbit to get started with:

   1: <SharePoint:DelegateControl ID="DelegateControl5" runat="server" ControlId="SmallSearchInputBox" />

What is this thing called SharePoint:DelegateControl?  Well, it looks like it is the product group’s version of SmartPart.  Any control that is registered can be instantiated by this syntax:

   1: <SharePoint:DelegateControl ID="[MyUniqueControlId]" runat="server" ControlId="[MyControlName]" />

Here is another tidbit. If you want to deploy a custom master page and use a custom style sheet, create a feature that looks like this:

   1: <?xml version="1.0" encoding="utf-8"?>
   2: <Feature  Id="e6070a56-757f-4a9c-842c-030e596a08d0"
   3:           Title="IpmMasterPages"
   4:           Description="Installs a custom master page for the Innovation Administration Site"
   5:           Version="12.0.0.0"
   6:           Hidden="FALSE"
   7:           Scope="Web"
   8:           DefaultResourceFile="core"
   9:           xmlns="http://schemas.microsoft.com/sharepoint/">  
  10:     <ElementManifests>
  11:       <ElementManifest Location="elements.xml" />
  12:       <ElementFile Location="Masters\ipmadministration.master" />
  13:       <ElementFile Location="Masters\ipm.css" />
  14:     </ElementManifests>  
  15: </Feature>

Elements.xml looks like this:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
   3:   <Module Name="IpmMasterPages" Url="_catalogs/masterpage" Path="Masters" RootWebOnly="FALSE">
   4:     <File Url="ipmadministration.master" Type="GhostableInLibrary" />
   5:   </Module>
   6:   <Module Name="IpmStyles" Url="Styles" Path="Masters" RootWebOnly="FALSE">    
   7:     <File Url="ipm.css" />
   8:   </Module>
   9: </Elements>

NOTE: Remember that the style sheet is NOT a list item, so if you copy the <File> element from the master module and don’t forget to remove the Type=”GhostableInLibrary” attribute, you will get an error when you create the site.    

And add a link to your style sheet in the master page

   1: <link rel="stylesheet" href="../../styles/ipm.css" type="text/css" />

In onet.xml, activate the custom master page feature:

   1: <!-- Custom master page-->
   2: <Feature ID="e6070a56-757f-4a9c-842c-030e596a08d0" />

Setup a feature receiver for the custom master age  and update the master page url on activation:

   1: using (SPWeb web = (SPWeb)properties.Feature.Parent)
   2: {
   3:     web.MasterUrl = web.MasterUrl.Replace("default.master", "ipmadministration.master");                        
   4:     web.Update();
   5: }

Happy SharePointing….

Posted by coafrica | 1 Comments
Filed under:

Learning SharePoint – Part III

Except for a project I did last year, I haven’t done much ASP.NET development.  I have always been a back-end developer. Writing Windows services, utilities, ETL, security systems, others… So, while I am learning about SharePoint development, I am also learning a great deal about ASP.NET development in general.  This is very exciting for me because while I love back end development, all my friends have done cool web applications so I wanna play too!

I just ordered Inside Microsoft Windows SharePoint Services, by Ted Pattison and Daniel Larson.  Two chapters are available online, which I came across while trying to understand how SharePoint uses master pages.  The book recommends opening C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\GLOBAL\default.master and learning about now it works.  In fact, this is the quote from the book “It's definitely worth your time to open up a copy of the default.master file within Microsoft Visual Studio and try to absorb all that's there.”

Okay, well, I am down for that.  

@Register

The first few lines of the master page look like this:

   1: <%@ Master Language="C#" %>
   2:  
   3: <%@ Register TagPrefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
   4: <%@ Register TagPrefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
   5: <%@ Import Namespace="Microsoft.SharePoint" %>
   6: <%@ Import Namespace="Microsoft.SharePoint.ApplicationPages" %>

The first line’s purpose is apparent, but what does <% @Register…%> mean?  Here is where my ignorance comes into play.  I did a ton of web development back in the Classic ASP days so the old <% %> tags look agonizingly familiar.  Surely not the same thing, right?

No, but the family tree is apparent.  Oh, by the way, as I read about ASP.NET Page Syntax, I branched off to learn something about DOCTYPES (great article).  It is interesting to note the effect DOCTYPE has on how a browser renders a page and how validation is also effected.  In particular, the DOCTYPE commonly cut and pasted from w3c’s web site:

   1: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
   2: "DTD/xhtml1-strict.dtd">

Is actually totally useless because the .dtd it references is a on path relative to w3c’s website!  Use this instead:

   1: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
   2: "http://www.w3.org/TR/html4/strict.dtd">

So, back to <% @Register…%>.  The <% %> signals to ASP.NET that it should pay attention to the enclosed text.  That’s about as simple as I can describe it.

When you use a control, such as a panel control, you use the syntax <asp:panel> </asp:panel>.

<% @Register…%> allows the developer to associate an alias with namespaces and classes so that you can essentially replicate the <asp:?> </asp:?> declaration but with custom user controls.  So, if I have a control called OrderWidget, which collects order information on a page, I can use <% @Register…%> to create an alias to the OrderWidget control and declaratively include the control in my page:

   1: <@ Register tagprefix=”Mcs”, tagName=”OrderWidget”, src=”~/OrderWidgetControl.ascx” %>
   1: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
   2: "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
   3:  
   4: <html>
   5: <head runat="server">
   6:     <title>Order Page</title>
   7: </head>
   8: <body>
   9:   <form id="form1" runat="server">
  10:     <Mcs:OrderWidget runat="server" />
  11:   </form>
  12: </body>
  13: </html>

In SharePoint's default.master, this is used extensively.  Here is an example:

   1: <%@ Register TagPrefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

And further down:

   1: <WebPartPages:SPWebPartManager ID="m" runat="Server" />

A key difference from my simple example is the use of an AssemblyName, which relates the prefix to an assembly versus as .ascx file.

<html dir=…>

Default.master’s opening html tag looks like this:

   1: <html dir="<%$Resources:wss,multipages_direction_dir_value%>" runat="server" xmlns:o="urn:schemas-microsoft-com:office:office">

The “dir” attribute indicates to the browser which direction text should run, which is important because many languages run right to left, versus English’s left to right. 

Note the <%Resources:wss, multipages …%>.  The <%$Resources…%> declaration is interesting.  ASP.NET expressions (prefixed by the $ sign) is a powerful technology which enables the declarative setting of properties based on information evaluated at run time.  In this case, the $Resources expression allows the loading of string resource information from a .resx file.  In the example above, a wss.resx exists (which can be found in the 12 hive at C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\CONFIG\Resources), which has a resource “multipages_direction_dir_value” defined:

   1: <data name="multipages_direction_dir_value">
   2:   <value>ltr</value>
   3: </data>

Pretty nifty!  More later…

Posted by coafrica | 0 Comments
Filed under:

mpFx 1.0 Preview Released to MSDN Code Gallery

The preview of mpFx 1.0 has been released to MSDN Code Gallery.  The tool I built to showcase some of the library’s functionality, mpFxClient, is installed along with the source.  Get it here.

I want to be clear that this is a preview release.  None of the tools, plugins, or source code is supported by Microsoft.  It is my hobby project (one of like twelve, lol).  MpFx is intended to be a learning aide for those tasked with building solutions with Microsoft’s EPM stack.  A bunch of stuff isn’t done yet, but I committed to releasing it and like any good hobby project, it will never really be finished!   The list of known issues will come out this week, so wait before flooding me with bug reports!  The exception is please let me know if you are having problems downloading plugins.

The plugins work but haven’t been tested much beyond my own use of them.

That said, I hope this is useful!

Getting Started with mpFx

After installation, check your Start menu for Microsoft Consulting Services\mpFx.  There are three shortcuts: mpFxClient, mpFx 1.0 Preview Release - Source.zip, and User Guide.  The user guide opens to a blank document.  That will be complete by the final release of 1.0.

The source shortcut opens to a zip archive of the source files.   Open the solution file in the source root directory to get started.

Auto-documentation of limited value, can be found here: http://mpfx.colbyafrica.com/docs/index.html.  The final release will include more documentation.

Running the mpFxClient Application

imageClick the mpFxClient shortcut to begin.  The main window is displayed, as shown to the right.  Click the image button to open the profiles dialog, which reads Microsoft Project Pro profile information from the registry.  If you don’t have Pro installed, you can create custom profiles and store them for future use.

MPFX 1.0 HAS NOT BEEN TESTED WITH FORMS AUTHENTICATION.

Finish inputting credential information and click the Log On button to begin.  Plugins are not installed by default, so the first thing to do is open the Plugin Gallery Browser to download plugins.

Click the image  menu item to open the browser.   NOTE: I switched hosting providers last night.  While I have tested this quite a bit, I am still getting to know this new provider.  Be patient if there are download problems but let me know immediately.

Continued below…

 

 

 

 

 

 

 

 

image The plugin gallery, as shown to the right, provides browsing and downloading functionality for plugins.  Select the plugin you want to download and click the image button to download and install it.  Once installed, you will see addition menu items in the main window’s toolstrip.

I will follow up this week with additional documentation, code samples, and tips and tricks.  For now, if you want to get started with the source, the easiest thing to do is look at the source code for one of the plugins.

I am frankly just a little beat and I still have work to do on an internal project today!

A few credits here:

Zip library for .NET is SharpZipLib.

Icons by FamFamFam.

Mike Shughrue for the event handler installation basics.

Brian Smith, Chris Boyd, Christophe Fiessinger, and Michael Jordan for helping me over the humps.

The International Institute of Learning for assigning me to a project that required in the PSI in the first place!

heart And, most important, my wife for letting me work all those long hours at night and on the weekends.   Now we have to do it all over again with the book!

 

 

ENJOY!

Posted by coafrica | 1 Comments
Filed under:

mpFx 1.0 Preview Release – Update II

Not that anybody is reading this, but I like to write :-)

A while back, I reserved the .mpfx.org domain.    For now, I have it forwarding to colbyafrica.com.  Check it out here.  My personal website is hosted by Yahoo Small Business, which uses technology other than the Microsoft stack.  As you might imagine, I am far less skilled in the world of LAMP than I am .Net.  The new domain, .mpfx.org, is hosted by DiscountAsp, which full implements the Microsoft stack, including IIS 7 on Windows Server 2008.

Very sweet.

The mpFxClient accesses an online gallery of plugins, which originally were hosted on colbyafrica.com.   This was a problem because my personal site just redirects to my blog and doesn’t allow for much bandwidth usage, plus I just don’t like the technology that much.  I moved it over to mpfx.org tonight.

Which caused a bit of a delay in releasing the preview.

I am testing it all now, but I hope to release tomorrow.  I don’t have a lot of time to spend on this tomorrow, so we will see how it goes.

One way or another, I have to get this pushed out because I am on a special project that won’t afford me much time to work these side projects.

Posted by coafrica | 0 Comments
Filed under:

mpFx 1.0 Preview Release: Update

I am working hard today to get mpFx ready for Code Gallery.  I might make it. I have work time scheduled for tomorrow but it is for Microsoft, not for my personal projects.  We will see how it goes this weekend.

Here is a very cool tool called Doxygen that generates documentation.

Here is a preview of the mpFx documentation (this will never be complete, read this).

Here are some other data points about mpFx:

Code Metrics

image

Lines of Code

image

Posted by coafrica | 1 Comments

Learning SharePoint – Part II

I have made some serious progress since my last post.  I found this very handy extension method, which adds a TryGetValue to SPListItem.

   1: // Credits: http://blog.mastykarz.nl/obtain-fields-values-splistitem/comment-page-1/#comment-11076
   2: internal static bool TryGetValue<T>(this SPListItem listItem, string fieldName, out T value)
   3: {
   4:     value = default(T);
   5:  
   6:     try
   7:     {
   8:         object candidate = listItem[fieldName];
   9:  
  10:         if (candidate == null)
  11:         {
  12:             return false;
  13:         }
  14:         value = (T) candidate;
  15:  
  16:         return true;
  17:     }
  18:     catch 
  19:     {
  20:         return false;
  21:     }
  22: }

The credit goes to Waldek.  While this isn’t earth shattering,  it is proof that tooling up goes a long way in increasing developer productivity.

   1: if (idea.TryGetValue("Title", out title))
   2: {
   3:     ideaTitleLabel.Text = title;
   4: }
   5: else
   6: {
   7:     ThrowMissingFieldException("title");
   8: }

I am building out my own kit of utilities as well.  Here is a utility for generating a DropDownList of all lists in a web:

   1: internal static DropDownList GetWebSpList(string url, Cache cache, CacheOptions cacheOptions)
   2: {
   3:     DropDownList dropDownList;
   4:  
   5:     using (DataSet listsDataSet = GetSpLists(url, cache, cacheOptions))
   6:     {
   7:         dropDownList = new DropDownList();
   8:  
   9:         foreach (DataRow row in listsDataSet.Tables[IPM_LIST_TABLE].Rows)
  10:         {
  11:             ListItem item = new ListItem(row["Title"].ToString(), row["ID"].ToString());
  12:  
  13:             dropDownList.Items.Add(item);
  14:         }
  15:     }
  16:  
  17:     return dropDownList;
  18: }

The method GetSpLists, looks like this:

   1: private static DataSet GetSpLists(string url, Cache cache, CacheOptions cacheOptions)
   2: {
   3:     DataSet listCache = null;
   4:  
   5:     if ((cacheOptions & CacheOptions.ReadFromCache) == CacheOptions.ReadFromCache)
   6:     {
   7:         listCache = cache[IPM_LIST_CACHE] as DataSet;
   8:     }
   9:  
  10:     if (listCache == null || (cacheOptions & CacheOptions.ReadLive) == CacheOptions.ReadLive)
  11:     {
  12:         listCache = CreateListCacheDataSet();
  13:  
  14:         using (SPSite site = new SPSite(url))
  15:         using (SPWeb web = site.OpenWeb())
  16:         {
  17:             SPListCollection lists = web.Lists;
  18:  
  19:             foreach (SPList list in lists)
  20:             {
  21:                 listCache.Tables[IPM_LIST_TABLE].Rows.Add(list.ID, list.Title);
  22:             }
  23:         }
  24:  
  25:         if ((cacheOptions & CacheOptions.AddToCache) == CacheOptions.AddToCache)
  26:         {
  27:             cache.Add(IPM_LIST_CACHE,
  28:                       listCache,
  29:                       null,
  30:                       Cache.NoAbsoluteExpiration,
  31:                       new TimeSpan(0, 0, 30, 0),
  32:                       CacheItemPriority.BelowNormal, null);
  33:             
  34:         }
  35:     }
  36:     return listCache;
  37: }

And CreateListCacheDataSet looks like this:

   1: private static DataSet CreateListCacheDataSet()
   2: {
   3:     DataSet listCache = new DataSet(IPM_LIST_TABLE);
   4:  
   5:     listCache.Tables.Add(IPM_LIST_TABLE);
   6:  
   7:     DataColumn primaryKeyColumn = listCache.Tables[IPM_LIST_TABLE].Columns.Add("ID", typeof(Guid));
   8:  
   9:     listCache.Tables[IPM_LIST_TABLE].PrimaryKey = new[] { primaryKeyColumn };
  10:  
  11:     listCache.Tables[IPM_LIST_TABLE].Columns.Add("Title", typeof(string));
  12:  
  13:     return listCache;
  14: }

And finally, the CacheOptions enum, which drives whether the lists are read from the cache or read live, and drive whether the resultant list is cached or not, looks like this:

   1: [FlagsAttribute]
   2: internal enum CacheOptions
   3: {
   4:     None,
   5:     ReadFromCache,
   6:     AddToCache,
   7:     RemoveFromCache,
   8:     ReadLive
   9: }
Posted by coafrica | 1 Comments
Filed under:
More Posts Next page »
 
Page view tracker