MOSS MVP

I've moved my blog to http://blog.falchionconsulting.com!. Please update your links. This blog is no longer in use--you can find all posts and comments at my new blog; I will no longer be posting to this site and comments have been disabled.

Friday, November 16, 2007

Update List View

One of the things I needed to do in order to get my Site Directory set up and working properly was to be able to modify the view that controls which columns appear when creating a new site collection. The way SharePoint determines which columns to use is via a special view that is created when you set up a site directory. The name of the view is "Site Creation Categories" and you can see it here: "http://[portal]/SiteDirectory/SitesList/Capture.aspx". If you want to control which columns appear when creating a new site then simply edit this view and add or remove those columns. Of course, I'm writing a batch file to make all these modifications so I needed to be able to do this via STSADM.

As you might imagine there is no built-in command to edit views so I had to create my own which I called gl-updatelistview. I started out pretty simple - just made it so that you could control the visibility of columns but then I decided that I'd try to add more so as to make it potentially more useful. In the end you can modify just about everything via this command that you could otherwise modify via the browser (there are some things I left out such as the Totals column settings). I thought about maybe taking it further and creating the ability to create a view as well but at present I don't have that need so I only did the update (it wouldn't be a stretch to create a createlistview command if needed).

The piece of functionality that messed me up the most was the setting of the View Style. It's easy enough to set the style but if you want to change the setting back to the "Default" setting there's simply no way to do this via the API. What I ended up having to do was use reflection to get the internal XmlDocument object where all the settings are stored and then manually remove the <ViewStyle /> node. Ugly I know but its the only way I could find to do it. The code can be seen below:

   1: /// <summary>
   2: /// Runs the specified command.
   3: /// </summary>
   4: /// <param name="command">The command.</param>
   5: /// <param name="keyValues">The key values.</param>
   6: /// <param name="output">The output.</param>
   7: /// <returns></returns>
   8: public override int Run(string command, StringDictionary keyValues, out string output)
   9: {
  10:  output = string.Empty;
  11:  
  12:  InitParameters(keyValues);
  13:  
  14:  string url = Params["url"].Value;
  15:  
  16:  using (SPSite site = new SPSite(url))
  17:  using (SPWeb web = site.OpenWeb())
  18:  {
  19:   SPList list = Utilities.GetListFromViewUrl(web, url);
  20:   if (list == null)
  21:     throw new Exception("List not found.");
  22:  
  23:   SPView view = null;
  24:   foreach (SPView v in list.Views)
  25:   {
  26:    if (url.ToLower() == SPEncode.UrlDecodeAsUrl(web.Site.MakeFullUrl(v.ServerRelativeUrl)).ToLower())
  27:    {
  28:     view = v;
  29:     break;
  30:    }
  31:   }
  32:   if (view == null)
  33:    throw new Exception("View not found.");
  34:  
  35:   if (Params["visiblefieldorder"].UserTypedIn)
  36:   {
  37:    view.ViewFields.DeleteAll();
  38:    foreach (string s in Params["visiblefieldorder"].Value.Split(','))
  39:    {
  40:     view.ViewFields.Add(s);
  41:    }
  42:   }
  43:  
  44:   if (Params["viewname"].UserTypedIn)
  45:    view.Title = Params["viewname"].Value;
  46:  
  47:   if (Params["mobileview"].UserTypedIn)
  48:    view.MobileView = bool.Parse(Params["mobileview"].Value);
  49:  
  50:   if (Params["defaultmobileview"].UserTypedIn)
  51:    view.MobileDefaultView = bool.Parse(Params["defaultmobileview"].Value);
  52:  
  53:   if (Params["rowlimit"].UserTypedIn)
  54:    view.RowLimit = uint.Parse(Params["rowlimit"].Value);
  55:  
  56:   if (Params["paged"].UserTypedIn)
  57:    view.Paged = bool.Parse(Params["paged"].Value);
  58:  
  59:   if (Params["scope"].UserTypedIn)
  60:    view.Scope = (SPViewScope)Enum.Parse(typeof(SPViewScope), Params["scope"].Value, true);
  61:  
  62:   if (Params["style"].UserTypedIn)
  63:   {
  64:    string viewStyle = Params["style"].Value;
  65:    SetStyle(web, view, viewStyle);
  66:   }
  67:  
  68:   if (Params["sort"].UserTypedIn)
  69:   {
  70:    string sortString = Params["sort"].Value;
  71:    SetSort(view, sortString);
  72:   }
  73:  
  74:   if (Params["filter"].UserTypedIn)
  75:   {
  76:    string caml = Params["filter"].Value;
  77:    SetFilter(view, caml);
  78:   }
  79:  
  80:   if (Params["group"].UserTypedIn)
  81:   {
  82:    string groupString = Params["group"].Value;
  83:    SetGroups(view, groupString);
  84:   }
  85:  
  86:   bool? collapseGroups = null;
  87:   int? groupLimit = null;
  88:   if (Params["collapsegroups"].UserTypedIn)
  89:    collapseGroups = bool.Parse(Params["collapsegroups"].Value);
  90:   if (Params["grouplimit"].UserTypedIn)
  91:    groupLimit = int.Parse(Params["grouplimit"].Value);
  92:  
  93:   SetGroupAttributes(view, collapseGroups, groupLimit);
  94:  
  95:   if (Params["makedefault"].UserTypedIn)
  96:    view.DefaultView = bool.Parse(Params["makedefault"].Value);
  97:  
  98:   if (Params["hidden"].UserTypedIn)
  99:    view.Hidden = bool.Parse(Params["hidden"].Value);
 100:  
 101:   
 102:   view.Update();
 103:  }
 104:  
 105:  return 1;
 106: }
 107:  
 108: /// <summary>
 109: /// Sets the filter.
 110: /// </summary>
 111: /// <param name="view">The view.</param>
 112: /// <param name="caml">The caml.</param>
 113: private static void SetFilter(SPView view, string caml)
 114: {
 115:  XmlDocument sortDoc = new XmlDocument();
 116:  sortDoc.LoadXml(string.Format("<Query>{0}</Query>", view.Query));
 117:  
 118:  XmlElement where = (XmlElement)sortDoc.SelectSingleNode("//Where");
 119:  if (string.IsNullOrEmpty(caml))
 120:  {
 121:   if (where != null)
 122:   {
 123:    sortDoc.DocumentElement.RemoveChild(where);
 124:    view.Query = sortDoc.DocumentElement.InnerXml;
 125:   }
 126:  }
 127:  else
 128:  {
 129:   if (where != null)
 130:    sortDoc.DocumentElement.RemoveChild(where);
 131:  
 132:   view.Query = sortDoc.DocumentElement.InnerXml + caml;
 133:  }
 134: }
 135:  
 136: /// <summary>
 137: /// Sets the sort.
 138: /// </summary>
 139: /// <param name="view">The view.</param>
 140: /// <param name="sortString">The sort string.</param>
 141: private static void SetSort(SPView view, string sortString)
 142: {
 143:  XmlDocument sortDoc = new XmlDocument();
 144:  sortDoc.LoadXml(string.Format("<Query>{0}</Query>", view.Query));
 145:  
 146:  XmlElement orderBy = (XmlElement)sortDoc.SelectSingleNode("//OrderBy");
 147:  if (string.IsNullOrEmpty(sortString))
 148:  {
 149:   if (orderBy != null)
 150:    sortDoc.DocumentElement.RemoveChild(orderBy);
 151:  }
 152:  else
 153:  {
 154:   if (orderBy != null)
 155:    sortDoc.DocumentElement.RemoveChild(orderBy);
 156:  
 157:   orderBy = sortDoc.CreateElement("OrderBy");
 158:   sortDoc.DocumentElement.AppendChild(orderBy);
 159:  
 160:   foreach (string s in sortString.Split(','))
 161:   {
 162:    string[] s2 = s.Split('=');
 163:    string fieldName = s2[0];
 164:    string order = s2[1];
 165:  
 166:    SPField field = null;
 167:    try
 168:    {
 169:     field = view.ParentList.Fields.GetFieldByInternalName(fieldName);
 170:     
 171:    }
 172:    catch (ArgumentException)
 173:    {
 174:    }
 175:  
 176:    if (field == null)
 177:     throw new SPException(string.Format("The field '{0}' specified in the sort parameter was not found.  Make sure the case is correct.", fieldName));
 178:  
 179:    XmlElement fieldRef = sortDoc.CreateElement("FieldRef");
 180:    fieldRef.SetAttribute("Name", field.InternalName);
 181:    if (order.ToLowerInvariant() != "asc")
 182:     fieldRef.SetAttribute("Ascending", "FALSE");
 183:  
 184:    orderBy.AppendChild(fieldRef);
 185:   }
 186:  }
 187:  view.Query = sortDoc.DocumentElement.InnerXml;
 188: }
 189:  
 190: /// <summary>
 191: /// Sets the group attributes.
 192: /// </summary>
 193: /// <param name="view">The view.</param>
 194: /// <param name="collapseGroups">The collapse groups.</param>
 195: /// <param name="groupLimit">The group limit.</param>
 196: private static void SetGroupAttributes(SPView view, bool? collapseGroups, int? groupLimit)
 197: {
 198:  if (!collapseGroups.HasValue && !groupLimit.HasValue)
 199:   return;
 200:  
 201:  XmlDocument groupDoc = new XmlDocument();
 202:  groupDoc.LoadXml(string.Format("<Query>{0}</Query>", view.Query));
 203:  
 204:  XmlElement groupBy = (XmlElement)groupDoc.SelectSingleNode("//GroupBy");
 205:  
 206:  if (groupBy == null)
 207:   throw new SPException("There are no group by columns set.  Cannot set collapsegroups or grouplimit.");
 208:  
 209:  if (collapseGroups.HasValue)
 210:   groupBy.SetAttribute("Collapse", collapseGroups.Value.ToString().ToUpper());
 211:  if (groupLimit.HasValue)
 212:   groupBy.SetAttribute("GroupLimit", groupLimit.Value.ToString());
 213:  
 214:  view.Query = groupDoc.DocumentElement.InnerXml;
 215: }
 216:  
 217: /// <summary>
 218: /// Sets the sort.
 219: /// </summary>
 220: /// <param name="view">The view.</param>
 221: /// <param name="groupString">The sort string.</param>
 222: private static void SetGroups(SPView view, string groupString)
 223: {
 224:  XmlDocument groupDoc = new XmlDocument();
 225:  groupDoc.LoadXml(string.Format("<Query>{0}</Query>", view.Query));
 226:  
 227:  XmlElement groupBy = (XmlElement)groupDoc.SelectSingleNode("//GroupBy");
 228:  if (string.IsNullOrEmpty(groupString))
 229:  {
 230:   if (groupBy != null)
 231:    groupDoc.DocumentElement.RemoveChild(groupBy);
 232:  }
 233:  else
 234:  {
 235:   bool collapse = true;
 236:   int groupLimit = 100;
 237:  
 238:   if (groupBy != null)
 239:   {
 240:    collapse = bool.Parse(groupBy.GetAttribute("Collapse"));
 241:    groupLimit = int.Parse(groupBy.GetAttribute("GroupLimit"));
 242:  
 243:    groupDoc.DocumentElement.RemoveChild(groupBy);
 244:   }
 245:  
 246:   groupBy = groupDoc.CreateElement("GroupBy");
 247:   groupDoc.DocumentElement.AppendChild(groupBy);
 248:  
 249:   groupBy.SetAttribute("Collapse", collapse.ToString().ToUpper());
 250:   groupBy.SetAttribute("GroupLimit", groupLimit.ToString());
 251:  
 252:   foreach (string s in groupString.Split(','))
 253:   {
 254:    string[] s2 = s.Split('=');
 255:    string fieldName = s2[0];
 256:    string order = s2[1];
 257:  
 258:    SPField field = null;
 259:    try
 260:    {
 261:     field = view.ParentList.Fields.GetFieldByInternalName(fieldName);
 262:    }
 263:    catch (ArgumentException)
 264:    {
 265:    }
 266:  
 267:    if (field == null)
 268:     throw new SPException(string.Format("The field '{0}' specified in the group parameter was not found.  Make sure the case is correct.", fieldName));
 269:  
 270:    XmlElement fieldRef = groupDoc.CreateElement("FieldRef");
 271:    fieldRef.SetAttribute("Name", field.InternalName);
 272:    if (order.ToLowerInvariant() != "asc")
 273:     fieldRef.SetAttribute("Ascending", "FALSE");
 274:  
 275:    groupBy.AppendChild(fieldRef);
 276:   }
 277:  }
 278:  view.Query = groupDoc.DocumentElement.InnerXml;
 279: }
 280:  
 281: /// <summary>
 282: /// Sets the view style.
 283: /// </summary>
 284: /// <param name="web">The web.</param>
 285: /// <param name="view">The view.</param>
 286: /// <param name="viewStyle">The view style.</param>
 287: private static void SetStyle(SPWeb web, SPView view, string viewStyle)
 288: {
 289:  bool styleFound = false;
 290:  foreach (SPViewStyle style in web.ViewStyles)
 291:  {
 292:   // Locate the style from the list of styles so that we can get the style ID.
 293:   if (!string.IsNullOrEmpty(style.Title) && style.Title.ToLowerInvariant() == viewStyle.ToLowerInvariant())
 294:   {
 295:    styleFound = true;
 296:    view.ApplyStyle(style);
 297:   }
 298:     
 299:  }
 300:  // If we didn't find the style and the user specified "default" then we need to clear the existing style setting.
 301:  // Problem is that there's no way to do this using public API calls so we have to manipulate the XML directly.
 302:  if (!styleFound && viewStyle.ToLowerInvariant() == "default")
 303:  {
 304:   // XmlDocument m_xdView = view.m_xdView;
 305:   FieldInfo m_xdViewProp = view.GetType().GetField("m_xdView",
 306:                BindingFlags.NonPublic |
 307:                BindingFlags.Instance |
 308:                BindingFlags.InvokeMethod |
 309:                BindingFlags.GetField);
 310:   XmlDocument m_xdView = (XmlDocument)m_xdViewProp.GetValue(view);
 311:   XmlElement viewStyleNode = (XmlElement)m_xdView.SelectSingleNode("//ViewStyle");
 312:   if (viewStyleNode != null)
 313:   {
 314:    viewStyleNode.ParentNode.RemoveChild(viewStyleNode);
 315:  
 316:    view.ViewHeader = null;
 317:    view.ViewBody = null;
 318:    view.ViewFooter = null;
 319:    view.ViewEmpty = null;
 320:    view.GroupByHeader = null;
 321:    view.GroupByFooter = null;
 322:   }
 323:  }
 324:  else if (!styleFound)
 325:   throw new SPException(string.Format("The View Style '{0}' was not found.", viewStyle));
 326: }
The syntax of the command can be seen below:
C:\>stsadm -help gl-updatelistview

stsadm -o gl-updatelistview

Updates a list view.

Parameters:
        -url <list view URL>
        [-visiblefieldorder <comma separated list of internal field names>]
        [-viewname <view name>]
        [-mobileview <true | false>]
        [-defaultmobileview <true | false>]
        [-rowlimit <number of items to display>]
        [-paged <true | false>]
        [-scope <default | recursive | recursiveall | filesonly>]
        [-style <view style>]
        [-sort <use the following format where the field name is the internal field name (leave blank to clear): "field1=asc|desc,field2=asc|desc">]
        [-filter <CAML XML (leave blank to clear)>
        [-group <use the following format where the field name is the internal field name (leave blank to clear): "field1=asc|desc,field2=asc|desc">]
        [-collapsegroups <true | false>]
        [-grouplimit <number of groups to display per page>]
        [-makedefault <true | false>]
        [-hidden <true | false>]
Here's an example of how to update a view using all of the above parameters:
stsadm -o gl-updatelistview -url "http://intranet/SiteDirectory/SitesList/Capture.aspx" -visiblefieldorder "DivisionMulti,RegionMulti" -viewname "Site Creation Categories" -mobileview false -defaultmobileview false -rowlimit 100 -paged true -scope recursive -style "Basic Table" -sort "DivisionMulti=asc,RegionMulti=asc" -filter "<Where><Or><Eq><FieldRef Name='DivisionMulti' /><Value Type='Text'>Operations</Value></Eq><Eq><FieldRef Name='DivisionMulti' /><Value Type='Text'>Human Resources</Value></Eq></Or></Where>" -group "DivisionMulti=asc" -collapsegroups false -grouplimit 100 -makedefault true -hidden false
Note that when specifying the CAML for the filter make sure that you wrap it in quotes and use tick marks for quotes in the XML otherwise you'll get command line errors.

27 comments:

Alpesh Nakar said...

Hi Gary,

Thanks for sharing this. Would it be possible to have single site map from across multiple site collections?

Gary Lapointe said...

Anything is possible :). It really just depends on what specifically you are needing. An stsadm command to do this would be pretty simple. For use in the browser you'd have to create a solution or feature containing whatever code you need (probably a web part or something).

Anonymous said...

Hey Gary,
Great set of tools! First thing I do at any client site (after getting permission) is to deploy your stsadm commands .wsp.

One thing, I'm having trouble getting -makedefault work with this command. The documentation and validation both specify it to be a switch, but it looks like the code is expecting it to have a true|false value. When I try to use it, I get 'Value cannot be null. Parameter name: value' error.

Thanks!

Gary Lapointe said...

Oops - nice catch - I'll have to fire my copy editor for that one :)

I've fixed the help comment and parameter validation and I'll try to get it pushed up today. In the meantime you should be able to pass true/false with the parameter and it should work.

Rao said...

Hi Gary,

I am facing probelm if I am using field name with edit menu like URL (Link to edit) in visablefileorder. could you please explain how can I use filed with edit menu.

Anonymous said...

I am using the gl-updatelistview to change what should be visible in the view. The problem is that the site does not show the change unless I click on the "Modify Shared Web Part" link and reselect the view.

Am I not setting something that pushes the changes to the site? Here is an example of the command I have set up:

stsadm -o gl-updatelistview -url "site_url/Forms/AllItems.aspx" -visiblefieldorder "desired fields" -viewname "All Documents" -mobileview true -defaultmobileview true -rowlimit 1000 -paged true -scope Default -style "Default" -sort "" -filter "" -makedefault true -hidden false

Gary Lapointe said...

Your issue sounds like it might be a caching issue - try recycling the app pool after you run the command.

Anonymous said...

I tried doing that with the command: cscript.exe c:\windows\system32\iisapp.vbs /a "UpgradeApplicationPool" /r but it did not change the site after I refreshed it. The iisreset also does not refresh the page. I have to do it manually in order for the view changes to be seen.

Gary Lapointe said...

That really is odd - I've used this command hundreds of times and never seen that behavior. What updates do you have installed in your farm?

Anonymous said...

I talked to the admin guy and he wanted to know what updates you are asking about. We are current in MOSS 2007 to SP1. Does that help?

Gary Lapointe said...

Do you have the infrastructure update and/or any cumulative updates or hot fixes installed?

Anonymous said...

I am waiting for a reply from the site admin, but I believe the answer is that we are up to date on all things.

Just to make sure we are talking about the same thing. I run the command and it works for the web part. When I look at the site as a whole, the changes are not visible. I then have to go into the web part by way of the 'Modified Shared Web Part'. I then select the view in the 'List Views' edit window to replace the 'Current View' with the new view. At this point the changes are visible.

So the question is does the command force the new view to be the 'current view'? If so, how is this done? If not, is there a way to do this in a stsadm command so I can do this programmatically. I do not want to have to step through each existing site to apply the changes to each web part manually.

Gary Lapointe said...

Duh - I'm sorry, I did misunderstand - this will not update any web parts using the view as web parts actually store a copy of the view you point to - they do not point to the actual view (You can see this if you use something like SharePoint Manager to navigate to the list - you'll see a view with a GUID for a name which corresponds to any list view web parts you are using). To change out a list view web part use the add list view web part command that I have to basically re-add the web part and use the set web part state command to remove the old web part.

Anonymous said...

Sorry not to get to you, I have been busy.

I have tried this latest solution and the results are the same. The web part shows 3 fields in the view, I want it to show 6 fields in the view.. I remove it, and add it again and it still shows 3 fields in the view.

What I need to do is refresh the current view for the web part with the new default view setting for the web part. So far I can only do this manually. I have 841 sites with 9 web parts per site that need to be refreshed to get the new 6 field set up to be visable. That is a lot of clicking.

Is this done by specifying the assembly value of the view? If so, I am not sure how to get that for each web part.

Gary Lapointe said...

Hmm...Odd, this should be using the default view when it adds the web part. At this point your best bet will be to write some PowerShell that iterates through all the web parts and manipulates the SPView object directly.

Anonymous said...

Not an option at this time. I can not get powershell loaded on the server by my admin.

Guess I will have to figure something else out. Thanks for the help and time.

Gary Lapointe said...

You could always write your own stsadm extension :)

Anonymous said...

Gary:
Thank you so much for all the automation. I am looking to create a non default view so that i can use the add list view webpart command and maintain the allitems view... right now, the update list view rights over the allitems view. Thanks... Patrick

Montanan Missing her Mountains said...

Gary,

I'm running into the same problem Rao has/had - I cannot add fields with an edit menu such as "Title (Link to edit item)... Any suggestions?

Gary Lapointe said...

How are you attempting to add the field?

Anonymous said...

I tried the command and it created the new view (perfect). It is visable in the web part after running the command. However, I want to set it as the "current view" on the main site page programmatically. Do you have a STSADM command to set the "Current View" in a site for a web part?

Montanan Missing her Mountains said...

Well here's the script I'm using: "stsadm -o gl-updatelistview -url http://connect.company.com/client/ZWANDC/Lists/Remote%20Access%20Information/At%20A%20Glance.aspx -visiblefieldorder "Label (linked to item with edit menu),Username,Password,Remote Access Type" -viewname "At a Glance" -mobileview true -defaultmobileview true -makedefault true"

The column exists in the list obviously. When I first used this script - it worked like a champ but then we realized we needed to add the field with the edit menu so we could actually access the record.

Gary Lapointe said...

You need to use the internal field names.

Montanan Missing her Mountains said...

Gary,

Thanks for replying and helping me with this. However, I'm unclear by what you mean "internal field names". How are they different from what I have and how do I look them up?

Gary Lapointe said...

Re: Current view - the list view web part uses a hidden view on the list. You'll need to re-add the web part through code or manually edit it via the UI.

Gary Lapointe said...

The internal name of the field is just the name that SharePoint uses to identify the field internally and it's usally less descriptive with no spaces. The easiest way to find the internal name is to download the SharePoint Manager tool from codeplex - you can then navigate to the field in the list and check the "InternalName" property (http://spm.codeplex.com).

Montanan Missing her Mountains said...

Gary,

Thank you SO much for that tip re: Sharepoint Manager. This was exactly the information I was looking for.