How do I programatically add an article page to a sharepoint site? - c#

I've been given the task of content migration from another CMS system to SharePoint 2010.
The data in the old system is fairly easy to capture and the page hierarchy is simple so I'm not worried about that.
However, I am completely flummoxed about how to even create a page in code. I'm using the Microsoft.SharePoint.Client namespace as I do not have sharepoint installed on my system and am wanting to code this up as a console application and so I'm using I'm using ClientContext. (On the other hand, I am willing to go into other solutions if necessary).
My end-game: To get a page uploaded into some folder hierarchy which uses a master page, has the page title in a header web part, and a big ol' content-editable web part in the body so any user can come along and edit the content.
Things I've tried so far:
Using FileCollection.Add() to add an aspx file to the folder "Site Pages". This renders the html in the browser but doesn't enable any features for the user to edit the page
Using ListItemCollection.Add() to add a page to the site, but I didn't know what fields I needed. Also I remember it came up with a runtime error saying I should use FileCollection.Add()
Uploading to 'Site Pages' instead of 'Pages'
So many others... ow my head :(
The only plausible thing I can see on the net is to use the PublishingPage type along with PublishingWeb. However, PublishingWeb can only be constructed from an SPWeb object which requires me to be actually hosting the sharepoint application on my workstation.
If anyone can lend a hand that would be greatly appreciated :)

Here is a method I use to create pages. It seems a more supported way of creating pages than mr Aquino's. Though this is for MOSS 2007 I'm sure the equivalent exists in 2010. Also, I'd recommend to create console apps using the full object model. You'll have to run it on the server itself but that doesn't seem much of a problem for a migration? This way you won't be limited in any way.
public static void CreatePage(string url, string pageName, string title, string layoutName, Dictionary<string, string> fieldDataCollection)
{
var relUrl = new Uri(url);
using (SPSite site = new SPSite(url))
using (SPWeb web = site.AllWebs[relUrl.AbsolutePath])
{
if (!PublishingWeb.IsPublishingWeb(web))
throw new ArgumentException("The specified web is not a publishing web.");
PublishingWeb pubweb = PublishingWeb.GetPublishingWeb(web);
PageLayout layout = null;
string availableLayouts = string.Empty;
foreach (PageLayout lo in pubweb.GetAvailablePageLayouts())
{
availableLayouts += "\t" + lo.Name + "\r\n";
if (lo.Name.ToLowerInvariant() == layoutName.ToLowerInvariant())
{ layout = lo; break; }
}
if (layout == null)
throw new ArgumentException("The layout specified could not be found. Available layouts are:\r\n" + availableLayouts);
if (!pageName.ToLowerInvariant().EndsWith(".aspx")) pageName += ".aspx";
PublishingPage page = pubweb.GetPublishingPages().Add(pageName, layout);
page.Title = title;
SPListItem item = page.ListItem;
foreach (string fieldName in fieldDataCollection.Keys)
{
string fieldData = fieldDataCollection[fieldName];
try
{
SPField field = item.Fields.GetFieldByInternalName(fieldName);
if (field.ReadOnlyField)
{
Console.WriteLine("Field '{0}' is read only and will not be updated.", field.InternalName);
continue;
}
if (field.Type == SPFieldType.Computed)
{
Console.WriteLine("Field '{0}' is a computed column and will not be updated.", field.InternalName);
continue;
}
if (field.Type == SPFieldType.URL)
{
item[field.Id] = new SPFieldUrlValue(fieldData);
}
else if (field.Type == SPFieldType.User)
{
// AddListItem.SetUserField(web, item, field, fieldData);
}
else
{
item[field.Id] = fieldData;
}
}
catch (ArgumentException)
{
Console.WriteLine("WARNING: Could not set field {0} for item {1}.", fieldName, item.ID);
}
}
page.Update();
}
}

I don't see a way of creating a publishing page without the actual publishing methods.
When you create a new article page it will only create a few xml parameters inside the page, the layout itself lives in the /_catalogs/masterpage/article-XXXX.aspx file.
You can try downloading a native file created in the Pages document library, understand its structure, fill the XML with your data and then uploading it back to the Pages document library using the FileCollection -- that's my only guess.
Edit: sample Article Page
<%# Page Inherits="Microsoft.SharePoint.Publishing.TemplateRedirectionPage,Microsoft.SharePoint.Publishing,Version=12.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c" %>
<%# Reference VirtualPath="~TemplatePageUrl" %>
<%# Reference VirtualPath="~masterurl/custom.master" %>
<html xmlns:mso="urn:schemas-microsoft-com:office:office" xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882"><head>
<!--[if gte mso 9]><xml>
<mso:CustomDocumentProperties>
<mso:PublishingContact msdt:dt="string">1073741823</mso:PublishingContact>
<mso:display_urn_x003a_schemas-microsoft-com_x003a_office_x003a_office_x0023_PublishingContact msdt:dt="string">System Account</mso:display_urn_x003a_schemas-microsoft-com_x003a_office_x003a_office_x0023_PublishingContact>
<mso:PublishingContactPicture msdt:dt="string"></mso:PublishingContactPicture>
<mso:PublishingContactName msdt:dt="string"></mso:PublishingContactName>
<mso:ContentTypeId msdt:dt="string">0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390078FB5FE740F6714B9595501175ECD8F000727044016EAB3B45B9E104498E366C85</mso:ContentTypeId>
<mso:Comments msdt:dt="string"></mso:Comments>
<mso:PublishingContactEmail msdt:dt="string"></mso:PublishingContactEmail>
<mso:PublishingPageLayout msdt:dt="string">http://dmserver008/_catalogs/masterpage/ArticlePage.aspx, EstudoAndre</mso:PublishingPageLayout>
</mso:CustomDocumentProperties>
</xml><![endif]--><title>New Article</title></head>
To grab one, hit the Pages library => Content Menu => Send To => Download a Copy

Uploading a page file should work, as long as you get the settings right on the item as well as the document itself. After you upload the file you can set the content type and properties appropriately. If you create a page manually first, you should be able to get an object that has all the right settings.
However, I would strongly recommend getting set up to develop a console app that will run on the sharepoint server rather than relying on the web services. The server side apis (including PublishingPage) tend to be a lot easier to work with.

Related

Read text from Sharepoint WebPart [c#]

I want to read text form my webpart. Its a simple one: just a line of text - nothing else.
I can get properties of my webparts (like title, desc etc) but can't get the content of it. Any ideas how to retreive this information? Thanks in advance.
using (SPSite site = new SPSite("http://mysite/pwa/some_web"))
{
using (SPWeb web = site.OpenWeb())
{
SPFile file = web.GetFile("default.aspx");
using (SPLimitedWebPartManager wpm = file.GetLimitedWebPartManager(System.Web.UI.WebControls.WebParts.PersonalizationScope.Shared))
{
foreach (Microsoft.SharePoint.Client.WebParts.WebPart wp in wpm.WebParts)
{
Console.WriteLine("Web part: {0}", wp.Title);
}
}
}
}
WebPart is the base class where other web part classes inherit from, and only contains the common properties such as Title. To read a specific property, you need to cast that wp object to the actual class of the web part.
Within the foreach loop:
MyWebPartClass mwp = (MyWebPartClass)wp;
Console.WriteLine(wp.SomeProperty);
Where MyWebPartClass inherits WebPart and declares the property SomeProperty. Note that you should also check first that the wp object is in fact a webpart of your class, in case there are more web parts on the same page

Sharepoint Sandbox solution - Updating Sitepages/Home.aspx on webprovisioned event, Occasional conflict error

As part of a sandbox solution I am creating, I have a web provisioned event receiver that does the following:
Updates the new sites branding to match the root site
Updates the default sitepages/home.aspx (if it exists)
Deletes the OOTB default.aspx (if the site has a sitepages/home)
This all seems to work ‘most’ of the time, but occasionally when creating a sub site the following error message appears:
Error - The file SitePages/Home.aspx has been modified by xxx#xxx on 02 Oct 2012 08:51:36 -0700.
It doesn’t happen all the time which makes it really strange to understand and debug. It almost appears to happen if you create a site to quick after creating another?
Can anyone help me understand why this might be happening. It is worth noting that I am on SharePoint Online so can not check the correlation ID.
public override void WebProvisioned(SPWebEventProperties properties)
{
// Get and set child and top sites
SPWeb childSite = properties.Web;
SPWeb topSite = childSite.Site.RootWeb;
// Apply branding from top site to childsite
childSite.MasterUrl = topSite.MasterUrl;
childSite.CustomMasterUrl = topSite.CustomMasterUrl;
childSite.AlternateCssUrl = topSite.AlternateCssUrl;
childSite.SiteLogoUrl = topSite.SiteLogoUrl;
childSite.Update();
// Construct HTML for new home.aspx page
string content = "Test Content";
// Check if the newsite has a sitepages library and home.aspx
SPFile oFile = childSite.GetFile("Sitepages/Home.aspx");
if (oFile.Exists)
{
// replace page content with new html
oFile.Item["WikiField"] = content;
// Update
oFile.Item.Update();
// Delete old Default page.
SPFile oDefault = childSite.GetFile("default.aspx");
if (oDefault.Exists)
{
oDefault.Delete();
}
oDefault.Update();
}
}

Submitting Repeating InfoPath Table to Sharepoint list in Browser Enabled Form

Hopefully you can help. I am working on a Browser Enabled InfoPath 2010 form that lives in a Document Library on a SharePoint site (2007 and 2010). In this form there is a repeating table with data that needs to be captured for reporting purposes. The solution I have chosen is to use the built in SharePoint lists.asmx Web Service to write the lines in the repeating table to a separate list on the sane SharePoint site, in the same site collection. I used the steps here, http://msdn.microsoft.com/en-us/library/cc162745(v=office.12).aspx, as my baseline.
I have gotten everything set up and working when run directly from the InfoPath form client site. The form opens and, on submit, the rows in the repeating table are written to the SharePoint list without any problems. However, once I deploy the form through Central Admin and test, none of the rows in the repeating table are getting written to the list. There are no errors or anything to indicate a problem with the code, and a boolean field used to indicate whether or not a row has been uploaded is set to true.
My first thought is that there is a problem with the permissions somewhere. Maybe when the service is called from client side it passes my credentials but when run from the server through the Document Library it's using something different? Shouldn't it use the credentials that are used to gain access to the SharePoint (which are also my AD credentials)? Is there a way to specify the use of the current users AD credentials when calling the lists.asmx web service in the code?
Anyway, not really sure where else to go with this. Any searching I do comes up with the same how two documentation on submitting to a SharePoint list in general. Any help would be greatly appreciated. Below is the code I am using to accomplish this.
if (MainDataSource.CreateNavigator().SelectSingleNode("/my:myFields/my:ShippingInformation/my:ShipDate", NamespaceManager).Value != "")
{
// If Ship Date is not blank, upload items in Material used to SP List where ForQuote is False
// Quote Number, RMA Number, Ship Date, Unit S/N,
// Create a WebServiceConnection object for submitting
// to the Lists Web service data connection.
WebServiceConnection wsSubmit =
(WebServiceConnection)this.DataConnections["Material Web Service Submit"];
//Create XPathNodeIterator object for the new Material Lines
XPathNodeIterator MaterialLines = this.MainDataSource.CreateNavigator().Select("/my:myFields/my:RepairQuote/my:QuoteLines/my:QuoteLine", NamespaceManager);
int lineCount = 0;
foreach (XPathNavigator NewLines in MaterialLines)
{
lineCount += 1;
if (NewLines.SelectSingleNode(".//my:ForQuote", NamespaceManager).Value == "false" && NewLines.SelectSingleNode(".//my:LineSubmitted", NamespaceManager).Value == "false")
{
// Set the values in the Add List Item Template
// XML file using the values in the new row.
DataSources["AddListItemTemplate"].CreateNavigator().SelectSingleNode("/Batch/Method/Field[#Name='Title']", NamespaceManager).SetValue(NewLines.SelectSingleNode(".//my:lineItem", NamespaceManager).Value);
DataSources["AddListItemTemplate"].CreateNavigator().SelectSingleNode("/Batch/Method/Field[#Name='RMANumber']", NamespaceManager).SetValue(MainDataSource.CreateNavigator().SelectSingleNode("./my:myFields/my:EntitlementContainer/my:RMANumber", NamespaceManager).Value);
DataSources["AddListItemTemplate"].CreateNavigator().SelectSingleNode("/Batch/Method/Field[#Name='UnitSerialNumber']", NamespaceManager).SetValue(MainDataSource.CreateNavigator().SelectSingleNode("./my:myFields/my:EntitlementContainer/my:serialNumber", NamespaceManager).Value);
DataSources["AddListItemTemplate"].CreateNavigator().SelectSingleNode("/Batch/Method/Field[#Name='ShipDate']", NamespaceManager).SetValue(MainDataSource.CreateNavigator().SelectSingleNode("./my:myFields/my:ShippingInformation/my:ShipDate", NamespaceManager).Value);
DataSources["AddListItemTemplate"].CreateNavigator().SelectSingleNode("/Batch/Method/Field[#Name='OrderType']", NamespaceManager).SetValue(MainDataSource.CreateNavigator().SelectSingleNode("./my:myFields/my:RepairQuote/my:orderType", NamespaceManager).Value);
DataSources["AddListItemTemplate"].CreateNavigator().SelectSingleNode("/Batch/Method/Field[#Name='QuoteNumber']", NamespaceManager).SetValue(MainDataSource.CreateNavigator().SelectSingleNode("./my:myFields/my:RepairQuote/my:quoteNumber", NamespaceManager).Value);
DataSources["AddListItemTemplate"].CreateNavigator().SelectSingleNode("/Batch/Method/Field[#Name='LineQuantity']", NamespaceManager).SetValue(NewLines.SelectSingleNode(".//my:lineQuantity", NamespaceManager).Value);
DataSources["AddListItemTemplate"].CreateNavigator().SelectSingleNode("/Batch/Method/Field[#Name='LineNumber']", NamespaceManager).SetValue(lineCount.ToString());
// Set the value of Cmd attribute to "New".
DataSources["AddListItemTemplate"].CreateNavigator().SelectSingleNode("/Batch/Method/#Cmd", NamespaceManager).SetValue("New");
// Submit the new row.
wsSubmit.Execute();
NewLines.SelectSingleNode(".//my:LineSubmitted", NamespaceManager).SetValue("true");
}
}
}
I'm unsure why the code doesn't work when you deploy and call the lists web service in code. However I would suggest you to try debugging it to get to the root of the problem:
Step by Step – Debug InfoPath 2010 Forms Deployed on SharePoint 2010 Using Visual Studio 2010
Please try this out and step through it to see if it goes through the code as expected.
Well, I am not sure if it is the answer per se, but I believe the problem was related to security on the site. I got around this by using the SharePoint Object Model instead of the web service to create the list items (See http://www.bizsupportonline.net/browserforms/how-to-use-sharepoint-object-model-submit-data-infopath-browser-form-sharepoint-list.htm). With the SharePoint Object Model I am able to use AllowUnsafeUpdates. Also, where it took me quite a bit of time to get the web service set up, including the data connections, CAML file, etc. This way, however, only took a few minutes. Lesson learned, use the SharePoint Object Model if at all possible.
foreach (XPathNavigator NewLines in MaterialLines)
{
lineCount += 1;
if (NewLines.SelectSingleNode(".//my:ForQuote", NamespaceManager).Value == "false" && NewLines.SelectSingleNode(".//my:LineSubmitted", NamespaceManager).Value == "false")
{
using (SPSite site = SPContext.Current.Site)
{
if (site != null)
{
using (SPWeb web = site.OpenWeb())
{
// Turn on AllowUnsafeUpdates on the site
web.AllowUnsafeUpdates = true;
// Update the SharePoint list based on the values
// from the InfoPath form
SPList list = web.GetList("/Lists/InfoPathRtpItems");
if (list != null)
{
SPListItem item = list.Items.Add();
item["Title"] = NewLines.SelectSingleNode(".//my:lineItem", NamespaceManager).Value;
item["RMANumber"] = MainDataSource.CreateNavigator().SelectSingleNode("./my:myFields/my:EntitlementContainer/my:RMANumber", NamespaceManager).Value;
item["UnitSerialNumber"] = MainDataSource.CreateNavigator().SelectSingleNode("./my:myFields/my:EntitlementContainer/my:serialNumber", NamespaceManager).Value;
item["ShipDate"] = MainDataSource.CreateNavigator().SelectSingleNode("./my:myFields/my:ShippingInformation/my:ShipDate", NamespaceManager).Value;
item["OrderType"] = MainDataSource.CreateNavigator().SelectSingleNode("./my:myFields/my:RepairQuote/my:orderType", NamespaceManager).Value;
item["QuoteNumber"] = MainDataSource.CreateNavigator().SelectSingleNode("./my:myFields/my:RepairQuote/my:quoteNumber", NamespaceManager).Value;
item["LineQuantity"] = NewLines.SelectSingleNode(".//my:lineQuantity", NamespaceManager).Value;
item["LineNumber"] = lineCount.ToString();
item.Update();
}
// Turn off AllowUnsafeUpdates on the site
web.AllowUnsafeUpdates = false;
// Close the connection to the site
web.Close();
}
// Close the connection to the site collection
site.Close();
}
}
}

Can I run a ASPX and grep the result without making HTTP request?

How can I just make a function call, without URL, and without HTTP, to a simple ASP.NET file, and capture the byte stream it generated?
More background information,
I need a some kind of template can put a little logic inside, to render some INI like text files. I give up those libraries ported from Java and come up a solution of using ASP.NET for template engine. (I am NOT using it to build a website, not even a HTML.)
I have written a ASP.NET page (no WebForm, no MVC), which accept a XML POST, and it generate a long text file based on a set of simple but not too simple rules.
I generate the XML from DB objects, submit to the ASP page, grep the result and it works very well. However, the problem is that we want to use as a library, using by a WCF. Because of this, I failed to use a relative path and I have to store the URL of the ASP somewhere in the configuration, which I do not want to.
It will be hosted on a IIS server, but not called (at least not directly) from any frontend ASP, and will never called from end user.
PS. I was originally looking for a simple template engine for C#, but they are too old and not maintenance anymore, poor documentation, missing integrated editor/debugger, too simple, and the they might speak different languages.
PPS. I've also thought about T4, but it does not have a editor nor debugger in VS 2008.
You can run an ASPX page without IIS, without an HTTP message, if you build a host for the ASPNET runtime.
Example:
public class MyAspNetHost : System.MarshalByRefObject
{
public void ProcessRequest(string page)
{
var request = new System.Web.Hosting.SimpleWorkerRequest
(page, // the page being requested
null, // query - none in this case
System.Console.Out // output - any TextWriter will do
);
// this will emit the page output to Console.Out
System.Web.HttpRuntime.ProcessRequest(request);
}
public AppDomain GetAppDomain()
{
return System.Threading.Thread.GetDomain();
}
}
public class Example
{
public void Run(IEnumerable<String> pages)
{
// ASPNET looks for assemblies - including the assembbly
// that contains any custom ASPNET host - in the bin\
// subdirectory of the physical directory that backs the
// ASPNET Host. Because we are going to use the current
// working directory as the physical backing directory for
// the ASPNET host, we need to ensure there's a bin
// subdirectory present.
bool cleanBin = false;
if (!Directory.Exists("bin"))
{
cleanBin = true;
Directory.CreateDirectory("bin");
}
// Now, ensure that the assembly containing the custom host is
// present in that bin directory. The assembly containing the
// custom host is actually *this* assembly.
var a = System.Reflection.Assembly.GetExecutingAssembly();
string destfile= Path.Combine("bin", Path.GetFileName(a.Location));
File.Copy(a.Location, destfile, true);
host =
(MyAspNetHost) System.Web.Hosting.ApplicationHost.CreateApplicationHost
( typeof(MyAspNetHost),
"/foo", // virtual dir - can be anything
System.IO.Directory.GetCurrentDirectory() // physical dir
);
// process each page
foreach (string page in pages)
host.ProcessRequest(page);
}
}
If you want to clean up that bin directory, you have to get the AppDomain to unload first. You can do that, like this:
private ManualResetEvent aspNetHostIsUnloaded;
private void HostedDomainHasBeenUnloaded(object source, System.EventArgs e)
{
// cannot clean bin dir here. The AppDomain is not yet gone.
aspNetHostIsUnloaded.Set();
}
private Run(IEnumerable<String> pages)
{
try
{
....code from above ....
}
finally
{
if (host!= null)
{
aspNetHostIsUnloaded = new ManualResetEvent(false);
host.GetAppDomain().DomainUnload += this.HostedDomainHasBeenUnloaded;
AppDomain.Unload(host.GetAppDomain());
// wait for it to unload
aspNetHostIsUnloaded.WaitOne();
// optionally remove the bin directory
if (cleanBin)
{
Directory.Delete("bin", true);
}
aspNetHostIsUnloaded.Close();
}
}
}
This makes sense for testing ASPX pages, and that sort of thing. But I'm not so sure this is the right thing, for your scenario. There are more direct ways to generate text files. But, it may be right for you. If you really like the template engine idea, hosting ASPNET may be just the thing for you.
In your case, you would want to modify the custom Host so that the output for each page goes to a StringWriter instead of Console.Out, and then you could do Grep (or more likely a search with Regex) on that output. You might also want to modify it to accept all the input data as a querystring. You'd need to format the page request to do that.
EDIT: There's a good article on MSDN Magazine on this technique of hosting the ASPNET runtime. From December 2004.
EDIT2: There's a simpler way to manage the bin directory. Just create a symbolic link named bin, pointing to ".". Then, you can remove the symlink after the call to AppDomain.Unload(), without waiting. Looks like this:
public void Run(string[] pages)
{
bool cleanBin = false;
MyAspNetHost host = null;
try
{
// This creates a symlink.
// ASPNET always looks for a bin\ directory for the privateBinPath of the AppDomain.
// This will create the bin dir, pointing to the current dir.
if (!Directory.Exists("bin"))
{
cleanBin = true;
CreateSymbolicLink("bin", ".", 1);
}
host =
(MyAspNetHost) System.Web.Hosting.ApplicationHost.CreateApplicationHost
( typeof(MyAspNetHost),
"/foo", // virtual dir - can be anything
System.IO.Directory.GetCurrentDirectory() // physical dir
);
foreach (string page in pages)
host.ProcessRequest(page);
}
finally
{
// tell the host to unload
if (host!= null)
{
AppDomain.Unload(host.GetAppDomain());
if (cleanBin)
{
// remove symlink - can do without waiting for AppDomain unload
Directory.Delete("bin");
}
}
}
}
This eliminates the need for the ManualResetEvent, copying files, synchronization, etc. It assumes the assembly for the custom ASPNet Host as well as all the assemblies required by the ASPX pages you run are contained in the current working directory.
This sounds like a very similar issue which is generating HTML emails on a server. There are some answers here that do that (for MVC):
ASP.NET MVC: How to send an html email using a controller?
You can proceed in a similar fashion for non-MVC by loading and rendering a control (ASCX) to a file.

how to add a WebPart to all pages in a SharePoint site?

I am using SharePiont Server 2007 Enterprise with Windows Server 2008 Enterprise, and I am using publishing portal template. I am developing using VSTS 2008 + C# + .Net 3.5. I want to know how to add a WebPart to all pages of a SharePoint Site? Any reference samples?
I want to use this WebPart to display some common information (but the information may change dynamically, and it is why I choose a WebPart) on all pages.
There are two ways to do this depending on your situation.
If the sites exist already, you need to iterate over the sites, adding the web part:
http://blogs.msdn.com/tconte/archive/2007/01/18/programmatically-adding-web-parts-to-a-page.aspx
If the sites do not exist then you can add the web part to the site template:
How to add a web part page to a site definition?
Here's the code from Shiraz's first link worked out a bit more:
(Note: This code is not optimized, for instance, looping through a List's Items collection is not something you should normally do, but seeing as this is probably a one time action there's no problem)
private void AddCustomWebPartToAllPages()
{
using(SPSite site = new SPSite("http://sharepoint"))
{
GetWebsRecursively(site.OpenWeb());
}
}
private void GetWebsRecursively(SPWeb web)
{
//loop through all pages in the SPWeb's Pages library
foreach(var item in web.Lists["Pages"].Items)
{
SPFile f = item.File;
SPLimitedWebPartManager wpm = f.GetLimitedWebPartManager(PersonalizationScope.Shared);
//ADD YOUR WEBPART
YourCustomWebPart wp = new YourCustomWebPart();
wp.YourCustomWebPartProperty = propertyValue;
wpm.AddWebPart(wp, "ZONEID", 1);
f.Publish("Added Web Part");
f.Approve("Web Part addition approved");
}
// now do this recursively
foreach(var subWeb in web.Webs)
{
GetWebsRecursively(subWeb);
}
web.Dispose();
}

Categories

Resources