Monday, April 23, 2007

Password Reminder updated!

I made some small (but significant) changes to the Password Reminder console application. First of all, it's useless to send disabled users a reminder to change their password eh? So I changed my code that only the 'not-disabled' users are notified. The property you need to query for this is : userAccountControl and it returns an integer. I used the following chart http://support.microsoft.com/kb/305144 to come up with the integer value of 544 to only get a non-disabled user.
The code below is the function that determines per user whether when his/her password will expire and if the user is enabled/disabled. If the password is about to expire within 20 days, an email will be sent to notify the user that their password must be changed.

public static void GetAccountExpiration(CUser user)

{

try

{

string LogUser = user.UserName;

DirectoryEntry entry = new DirectoryEntry("LDAP://DOMAINSERVER");

DirectorySearcher search = new DirectorySearcher(entry);

search.Filter = "(SAMAccountName=" + LogUser + ")";

SearchResult LDAPresult = search.FindOne();

entry = LDAPresult.GetDirectoryEntry();

//This int will tell us if the user is disabled yes or no

int disabled = Convert.ToInt32(entry.Properties["userAccountControl"].Value.ToString());

// Pulling the informtion on when the password was last changed and converting it to a LargeInteger.

LargeInteger liAcctPwdChange = entry.Properties["pwdLastSet"].Value as LargeInteger;

// Convert the highorder/loworder parts of the property pulled to a long.

long dateAcctPwdChange = (((long)(liAcctPwdChange.HighPart) << 32) + (long)liAcctPwdChange.LowPart);

// Convert FileTime to DateTime and get what today's date is.

DateTime dtNow = DateTime.Now;

// I added 90 days because I know what my password expiration is set to, if not you need to pull that information and add the number of days it is set for.

CultureInfo ci = new CultureInfo("nl-NL");

DateTime dtAcctPwdChange = DateTime.FromFileTime(dateAcctPwdChange).AddDays(180);

string strAcctPwdChange = DateTime.FromFileTime(dateAcctPwdChange).ToString("d",ci);

string strAcctPwdExpires = DateTime.FromFileTime(dateAcctPwdChange).AddDays(180).ToString("d",ci);

// Calculate the difference between the date the pasword was changed, and what day it is now and display the # of days.

TimeSpan time;

time = dtAcctPwdChange - dtNow;

if (time.Days < 20 && time.Days >= 0)

{

if (disabled == 544)

{

Console.WriteLine(user.FirstName + " " + user.LastName + " " + user.EmailAddress + " " + "Password changed: " + strAcctPwdChange + " Password expires: " + strAcctPwdExpires + " There are " + time.Days.ToString()+ " days left");

WebMail mail = new WebMail();

mail.To = user.EmailAddress;

mail.From = "";

mail.Subject = "Password expiration";

mail.Body = "" +

"<style>table {margin: 20px; border: 2 inset #efefff; background-color: #efefff; } td {font-family: verdana; font-size: 10px; color: #444444; font-weight: normal} .label {width:200px; font-weight: bold;} .title {font-weight: bold; font-size:14px; align: center;}</style>" +

"<table cellspacing=10 cellpadding=0>" +

"<tr><td colspan=2 class=title>Password expiration</td></tr>" +

"<tr><td class=label>Dear "+user.FirstName + " "+user.LastName +" , <BR><BR>your password is about to expire. You've last changed your password on " + strAcctPwdChange + ". Your password will expire or is expired on " + strAcctPwdExpires + ".<br></td></tr></table>";

mail.SendMessage();

}

}

}

catch(Exception error)

{

Console.WriteLine(error.Message.ToString());

}

}


 

And there you have it! Btw if you are wondering why on earth I created this console app.. Well, in the Sharepoint2003 time we did not have the luxury of formsbased authentication for our internet users so we created an extranet domain which could be administrated via the web. And when an user logons to Sharepoint there was no easy task of telling the user that his or her password was about to expire, even if it was possible, not every user logged on every day/week so the idea came to notify the user by email.

Saturday, April 21, 2007

Sharepoint and databases

Well funny thing happened these week, I wanted to create a (sub)web programmatically using the OM using a webtemplate. Nothing fancy here eh? :) Just what I thought so I build it and I wanted to test it and everyhing went ok. So I deployed on production environment, tested it there and then the problems came. It continually keep getting errors like 'cannot complete this action'. Things that did work :
1. The ID could be read via the OM
2. The title of the web could be read via the OM
Things that didn't work :
1. Kept getting 404's when I navigated to the site
2. Kept getting 'cannot complete this action' errors using the weburl and adding the /layouts/settings.aspx (or any other .aspx) urls
3. The navigation in WSS was broken, all the subsites could not be displayed.
4. Could not be deleted using the OM
5. Could not be deleted using STSADM

So what to do? Yes.. the one thing you should never do. Modify the database. It's unsupported by MS (default.aspx-scid=kb;EN-US;841057) as my mate Daniel also pointed out and told me to call support of MS to solve this issue. Although during our IM session I managed to fix it by modifying the record in the WEBS table and then I could delete the web using STSADM. The tricky part here is that you'd never know what happens in the future after this modification (although the records were gone after the deletion with STSADM, so technically speaking the modifications were not there anymore)

This brings me to the next thing I wanted to get of my chest and that is: Sharepoint developers/consultants must know what is going on in the databases. Having read the following blogpost SharePoint Database Growth Rates by Dave Wollerman about truncate the logfiles if the recovery model of the database is set to Full. By default the databases that are created by Sharepoint have the recovery model on Full. And why is that? I can't imagine why anyone wants to recover his/her Sharepoint database using the transactional logs since the database backup is probably the last backup/restore method you should use.
Therefore I always recommend setting the recovery model to Simple so you don't have to think about the transaction logfiles. Another benefit you get with this is better perfomance since the DMBS doesn't have to worry about creating logfiles. But to know all of this, just to let the DBA guys know how to configure, you have to get knowledge of SQL Server.

So to conclude this post: do some SQL Server training or even better get certified as MS SQL DBA (like I'm planning to be ;))

MOSS Extension :Interactive Media Manager

Well here is the first add-on for MOSS (if you don't count Excel- and Forms Services or the BDC as seperate add-ons). So what does this add-on do? Basically it's a nice solution for companies that want to use digital images as their primary content to collaborate with each other. Here is a snippet of the features :

Interactive Media Manager includes components of existing Microsoft solutions such as Windows Workflow Foundation, Windows Communication Foundation, InfoPath® XML forms and the newly announced Microsoft Silverlight™, previously called Windows Presentation Foundation Everywhere (WPF/E). Building on the capabilities of Office SharePoint Server 2007, Interactive Media Manager provides the following customizable Web parts that target specific processes within the content production life cycle:

Media Library serves as a front end in which media content items in the digital asset management system are displayed as thumbnails.

Media Viewer displays information such as annotations and time codes in Society of Motion Picture and Television Engineers format and offers sophisticated, built-in playback controls.

Media Annotator allows groups of people to collaborate on media assets through video annotations, digital inking and discussions.

Media Import facilitates the upload of media files and automatically launches associated workflows, such as information-gathering processes.

Media Cart provides an individualized area for storing media assets that people want to edit themselves or push out to appropriate editors.

 

(really nice theme they use eh? ;)

What really interests me is the following sentence "..Interactive Media Manager easily integrates with Microsoft business intelligence tools, giving management insight into production details...". Does anybody have thoughts about this? Here is more info from the whitepaper that answers my question :)

Business Intelligence

Interactive Media Manager offers first-class reporting capabilities with customizable dashboards and easy integration with reporting tools such as Microsoft Office PerformancePoint™ Server 2007, Microsoft SQL Server™ 2005 Reporting Services and Excel® Services. (Excel Services is a new, shared service in SharePoint Server 2007 that makes it easy to display and calculate Microsoft Office Excel 2007 workbooks on SharePoint Server portals and dashboards.) Media and management companies will want to take advantage of the built-in workflow reporting functions that show the status, time spent, and steps included in ongoing workflows.

Unlike standalone DAM systems, the Interactive Media Manager solution offers a convenient way of incorporating business information into content production. The Report Center offers a central place for collecting and publishing reports, spreadsheets, and key performance indicators common to a group, including SQL Server 2005 Reporting Services reports and data from business management systems. Also, with Excel Services, people can securely share live, interactive Excel workbooks.

Check http://www.microsoft.com/resources/mediaandentertainment/solutions_imm.mspx and Microsoft Introduces Interactive Media Manager - extension of MOSS 2007 for more details!

Tuesday, April 17, 2007

MOSS : Keeping metadata while copying/moving items

From the Sharepoint Helpdesk comes this post about how to keep your metadata while you want to copy or move items/documents across your sites/lists. The keyfactor here is the use Manage Content Structure link in the site adminstration page! In short, this is the Explorer view but then webbased ;)

So thanks Stephen! I had some difficulty with moving items around when a list became corrupted and I wanted to move all the items while the metadata was preserved, this is just what I needed back then ;)

Friday, April 13, 2007

SQL 2k > SQL 2k5 > SQL 2k = NoGo

Right you're probably wondering why on earth you would want to migrate a database from SQL2k to SQL2k5 and then back to SQL2k heh?
Well let me explain the context; a request to migrate an existing Sharepoint 2003 environment into another existing Sharepoint 2003 environment was made by a customer. No problem eh? Well it is when one environment is only RTM and the other is SP2. Luckily the to-be-migrated environment wasn't that big so I could restore the environment onto a development machine using the RTM version then apply SP2 and backup the environment again and restore that servicepackked environment to the destination environment.
So what was the problem you think.. well the 'from' environment was SQL2K, the 'upgrade-to-sp2' environment was SQL2K5 and the 'destination' environment was SQL2K. And while doing the whole process of installing Sharepoint on the 'upgrade-to-sp2' environment I had forgotten that SQL2K5 was running as primary database server.
When I wanted to restore the sp2'ed environment I immediatly received errors in the SPSBackup/Restore tool (ofcourse without a proper error-code). The next thing I tried was to restore the backup files straight into SQL where I was faced with some very weird errors like 'only 64 backup devices are allowed'.. Then it struck me! So I opened up the development machine, opened up SQL Management Studio and there were my SQL2K databases, upgraded to SQL2k5.. although they had SQL2000 compatablility level, it was not downgradable to be restored/attached to a SQL2k database server.

Thus long story short : it's not possible to restore (migrated) sql2k5 databases to sql2k databaseservers.

Thursday, April 05, 2007

WSS v3 : Anonymous access on document libraries

Update (24-9-2007), Doug Ware has posted another workaround for this problem. Check it out here

Ok, here is something I discovered yesterday. A mate of mine uses WSS for his personal use and he wanted that anonymous users could upload pictures into a picture library. Problem was that it was not possible! It was possible to create an announcement anonymously so that was really odd. Since a picture library is just a fancy list and an announcement list is not-so fancy list. But both still are basically a list and should have the same permissions actions.
So next step was to google the problem.. One of the first hits led me to : Enable anonymous access from the Office site. Scrolling through it, there was this note at the very bottom of the page..

Aha! So now googling (*cough* LIVEsearching *cough*) for the solution was much more easy ;)
Stumbling upon, Enabling anonymous users to open and submit data via InfoPath Forms published to SharePoint 2007 by Ervin Gayle checking step 13:

13) Take a look at the URL of this page you will notice that after the setanon.aspx page there is a query "obj=%listidhere....." followed by "DOCLIB" Surprise
14) If you change this last part of the URL from "DOCLIB" to "LIST", you will notice that you now have all of the option that you've been missing.

..was the answer to our problem!

I'm guessing (/hoping) there will be a hotfix that will enable administrators to explicitly enable/disable this feature.

Technorati tags: ,

Tuesday, April 03, 2007

WSS/SPS2003 : User Permissions Webpart

UPDATE! As promised here is the solution file : CrossSiteGroupWebPart.rar
And if you just want to install the webpart : CrossSiteGroupWebPartInstall.CAB
And this the list-template you will need : AnalyseNon.stp

I got the question from a customer who wanted to have a view of every user on a site with all the permissions that the user had per list (since you can set permissions per list). Sounds pretty forward huh? :) One challenge I had to face lies in the fact that this customer is a big fan of creating custom sitegroups and cross-site groups.

Another (personal) challenge was that I wanted to publish this information in a custom list on the site where the webpart is dropped upon. You may wonder why I want to publish the information back in Sharepoint again. Well there are 3 reasons : 1. Export to Excel (requirement of the customer) 2. Sorting/Filtertering/Group By of columns 3. Intergration with AD (user presence)

Thus, the first thing was to create a function to retrieve all the permissions per list for each user on a site.

private void GetSecurityForWeb(string url)

{

Stopwatch sp = new Stopwatch();

sp.Start();

ArrayList AnalyzeItems = new ArrayList();

SPSite site = new SPSite(url);

SPWeb web = site.OpenWeb();

try

{

Label1.Text += "<BR>Iterating through "+web.Title.ToString();

SPUserCollection users = web.Users;

foreach (SPUser user in users)

{

foreach (SPList list in web.Lists)

{

System.Collections.Specialized.StringCollection permissions = new System.Collections.Specialized.StringCollection();

if (!list.Permissions.Inherited)

{

SPPermissionCollection perms = list.Permissions;

foreach (SPPermission perm in perms)

{

if (user.ID == perm.Member.ID)

{

foreach (SPRole role in user.Roles)

{

if (role.Type != SPRoleType.None)

{

if (!permissions.Contains(role.Type.ToString()))

{permissions.Add(role.Type.ToString());}

}

}

}

else

{

foreach (SPRole role in user.Roles)

{

if (role.ID == perm.Member.ID)

{

if (role.Type != SPRoleType.None)

{

if (!permissions.Contains(role.Type.ToString()))

{permissions.Add(role.Type.ToString());}

}

else

{

if (!permissions.Contains(role.Name.ToString()))

{permissions.Add(role.Name.ToString());}

}

}

foreach (SPGroup group in role.Groups)

{

if (group.ID == perm.Member.ID)

{

if (!permissions.Contains(perm.PermissionMask.ToString()))

{permissions.Add(perm.PermissionMask.ToString());}

}

}

}

foreach (SPGroup group in user.Groups)

{

if (group.ID == perm.Member.ID)

{

if (!permissions.Contains(perm.PermissionMask.ToString()))

{permissions.Add(perm.PermissionMask.ToString());}

}

}

}

}

}

else

{

foreach (SPRole role in user.Roles)

{

if (role.Type != SPRoleType.None)

{

if (!permissions.Contains(role.Type.ToString()))

{permissions.Add(role.Type.ToString());}

}

else

{

if (!permissions.Contains(role.Name.ToString()))

{permissions.Add(role.Name.ToString());}

}

foreach (SPGroup group in role.Groups)

{

if (!permissions.Contains(group.Name.ToString()))

{permissions.Add(group.Name.ToString());}

}

}

}

//Populating of all the groups where the user is a member of

string groups = string.Empty;

int teller = 1;

foreach (SPGroup group in user.Groups)

{

if (user.Groups.Count == teller)

{groups += group.Name.ToString();}

else

{groups += group.Name.ToString() + " " ;}

teller++;

}

//Adding all the information in an object and pushing this object in an ArrayList

AnalyzeItem item = new AnalyzeItem();

item.Site = web.Title;

item.Username = user.Name;

item.UserID = user.ID.ToString();

item.List = list.Title;

string _perm = string.Empty;

foreach (string perm in permissions)

{

_perm += " " + perm;

}

item.Permissions = _perm;

item.Group = groups;

AnalyzeItems.Add(item);

}

web.Close();

web.Dispose();

}

}

catch(Exception error)

{

Label1.Text += error.Message.ToString();

}

finally

{

site.Close();

site.Dispose();

}

sp.Stop();

Fetch = sp;

AddListItem(url, AnalyzeItems);

}

So what I do is gather all the permissions and store them in a custom class called AnalyzeItem which looks like this :

using System;

using System.Collections;

namespace CrossSiteGroupWebPart

{

/// <summary>

/// Summary description for AnalyzeItem.

/// </summary>

public class AnalyzeItem

{

private string _username = string.Empty;

private string _userid = string.Empty;

private string _site = string.Empty;

private string _list = string.Empty;

private string _group = string.Empty;

private string _permissions = string.Empty;

public AnalyzeItem()

{

//

// TODO: Add constructor logic here

//

}

public string Username

{

get

{

return _username;

}

set

{

_username = value;

}

}

public string UserID

{

get

{

return _userid;

}

set

{

_userid = value;

}

}

public string Site

{

get

{

return _site;

}

set

{

_site = value;

}

}

public string List

{

get

{

return _list;

}

set

{

_list = value;

}

}

public string Group

{

get

{

return _group;

}

set

{

_group = value;

}

}

public string Permissions

{

get

{

return _permissions;

}

set

{

_permissions = value;

}

}

}

}

So now we have all the data stored in the ArrayList, now we want to store this information into a List

private void AddListItem(string url, ArrayList items)

{

Label1.Text += "<BR>Pushing items into list";

Stopwatch sp = new Stopwatch();

sp.Start();

SPSite _Site = new SPSite(url);

SPWeb _Web = _Site.RootWeb;

try

{

Label1.Text += "<BR>Checking if list exists";

SPList analyse = _Web.Lists["Analyse"];

}

catch(Exception error)

{

Label1.Text += "<BR>List not found, therefore creating it";

CreateList(url);

Label1.Text += "<BR>List created";

}

finally

{

_Web.Close();

_Web.Dispose();

_Site.Close();

_Site.Dispose();

}

SPSite site = new SPSite(url);

SPWeb web = site.RootWeb;

SPWeb _compareWeb = site.OpenWeb();

try

{

Label1.Text += "<BR>Emptying list if items already exist";

SPList analyse = web.Lists["Analyse"];

if (analyse.Items.Count > 0)

{

for (int i=analyse.Items.Count-1; i>-1; i--)

{

if (analyse.Items[i]["Site"].ToString() == _compareWeb.Title.ToString())

{

analyse.Items.Delete(i);

}

}

}

Label1.Text += "<BR>List emptied!";

foreach (AnalyzeItem _item in items)

{

SPListItem item = analyse.Items.Add();

item["Title"] = _item.Username;

item["UserName"] = _item.UserID;

item["List"] = _item.List;

item["Permissions"] = _item.Permissions;

item["Groups"] =_item.Group;

item["Site"] = _item.Site;

item.Update();

}

}

catch (Exception error)

{

Label1.Text += "<BR>"+error.Message.ToString();

}

finally

{

web.Close();

web.Dispose();

site.Close();

site.Dispose();

}

sp.Stop();

Import = sp;

}

Now you may wonder what the function CreateList, as you may have guessed by the name it creates a custom list based on a customlist template. See the following code chunk on how I did this. I had some difficulties since the customer has a Portal + teamsites environment so every site beneath the portal is a sitecollection instead of a site. Because of this I could not use the custom list template since this is per site-collection and cannot be referenced over sitecollections. So I choose to physically copy the template from a source site (where the template can ben maintained as well) to the site where the webpart was dropped upon.

<

private void CreateList(string url)

{

//First copy the list template from the source site to the current site

SPSite siteCollection = new SPSite(sourcesite);

SPWeb web = siteCollection.OpenWeb();

SPWeb destweb = SPControl.GetContextWeb(Context);

try

{

SPFolder srcFolder = web.Folders["_catalogs"].SubFolders["lt"];

SPFile srcFile;

if (destweb.WebTemplate == "MPS")

{

srcFile = srcFolder.Files["Analyse.stp"];

}

else

{

srcFile = srcFolder.Files["AnalyseNon.stp"];

}

Label1.Text += "<BR>Template found! " + srcFile.Name.ToString();

try

{

Label1.Text += "<BR>Opening destination web";

SPFolder destFolder = destweb.Folders["_catalogs"].SubFolders["lt"];

SPFileCollection destFiles = destFolder.Files;

Label1.Text += "<BR>Filecollection opened";

string destURL = destFolder.Url + "/" + srcFile.Name;

byte[] binFile = srcFile.OpenBinary();

destFiles.Add(destURL, binFile);

Label1.Text += "<BR>Template saved to "+destURL.ToString();

}

catch(Exception _error)

{

Label1.Text+="<BR>Problem adding template" + _error.Message.ToString();

}

}

catch(Exception error)

{

Label1.Text += "<BR>List creation failed due to : " + error.Message.ToString();

}

finally

{

web.Close();

web.Dispose();

siteCollection.Close();

siteCollection.Dispose();

}

try

{

Label1.Text += "<BR>Trying to create list based on new template";

SPSite destSite = SPControl.GetContextSite(Context);

SPWeb destWeb = destSite.OpenWeb();

Label1.Text += "<BR>Opened the current web";

SPListTemplateCollection customListTemplates = destSite.GetCustomListTemplates(destWeb);

Label1.Text += "<br>#customtemplates" + customListTemplates.Count.ToString();

foreach(SPListTemplate temp in customListTemplates)

{

Label1.Text += "<BR>" + temp.Name.ToString();

}

if (destWeb.WebTemplate == "MPS")

{

SPListTemplate listTemplate = customListTemplates["Analyse"];

destWeb.Lists.Add("Analyse", "", listTemplate);

}

else

{

SPListTemplate listTemplate = customListTemplates["AnalyseNon"];

destWeb.Lists.Add("Analyse", "", listTemplate);

}

}

catch(Exception ___error)

{

Label1.Text += "<BR>" + ___error.Message.ToString();

}

}

As you may have noticed, I use different custom list templates when a different webtemplate is applied to the site. This has to do with site definitions are stored into a custom list template (see KB: Custom List Templates Do Not Appear on the "Create Page" Page of a Site) took me a couple of hours to figure this thing out btw. Since the file is physically there the frustation grew and grew when the object model said there was no custom list template to be found in the gallery :)

And finally we need to build the user interface where the user can select a site and press a button to populate the list. I embedded some javascript to show something an animated gif while the webpart is loading his stuff, so the user doesn't think that the webpart is not functioning. Also I built in a check that only webadmin's can run this tool, since some adminstrative privileges are required when userproperties are queried.

public class CrossSiteGroupWebPart : Microsoft.SharePoint.WebPartPages.WebPart

{

public Stopwatch Fetch;

public Stopwatch Import;

public StringBuilder sb = new StringBuilder();

LinkButton refreshButton;

private Label Label1;

public string nummer = "";

DropDownList drpSiteList;

private const string defaultText = "";

private string text = defaultText;

[Browsable(true),

Category("Miscellaneous"),

DefaultValue(defaultText),

WebPartStorage(Storage.Personal),

FriendlyName("Text"),

Description("Text Property")]

public string Text

{

get

{

return text;

}

set

{

text = value;

}

}

/// <summary>

/// This method gets the custom tool parts for this Web Part by overriding the

/// GetToolParts method of the WebPart base class. You must implement

/// custom tool parts in a separate class that derives from

/// Microsoft.SharePoint.WebPartPages.ToolPart.

/// </summary>

///<returns>An array of references to ToolPart objects.</returns>

// public override ToolPart[] GetToolParts()

// {

// ToolPart[] toolparts = new ToolPart[2];

// WebPartToolPart wptp = new WebPartToolPart();

// CustomPropertyToolPart custom = new CustomPropertyToolPart();

// toolparts[0] = wptp;

// toolparts[1] = custom;

// return toolparts;

// }

/// <summary>

/// Render this Web Part to the output parameter specified.

/// </summary>

/// <param name="output"> The HTML writer to write out to </param>

protected override void CreateChildControls()

{

nummer = this.ID.ToString();

Label1 = new Label();

this.Controls.Add(Label1);

refreshButton = new LinkButton();

refreshButton.Text="<img src='/_layouts/images/REFRESH.GIF' border='0' />Refresh overview";

refreshButton.Attributes.Add("onclick","this.style.display='none';"+

"var obj_msg = document.getElementById('waitmessage"+nummer+"'); "+

"if (obj_msg != null) obj_msg.style.display='';"+

"var obj_tasks = document.getElementById('tasks"+nummer+"'); "+

"if (obj_tasks != null) obj_tasks.style.display='none';" +

"javascript:setTimeout('UpdateImg"+nummer+"()');");

refreshButton.Click+=new EventHandler(refreshButton_click);

this.Controls.Add(refreshButton);

drpSiteList = new DropDownList();

try

{

drpSiteList.DataSource = GetSubWebs();

drpSiteList.DataTextField = "Title";

drpSiteList.DataValueField = "Url";

drpSiteList.DataBind();

}

catch(Exception error)

{

Label1.Text += "<BR>" + error.Message.ToString();

}

this.Controls.Add(drpSiteList);

base.CreateChildControls ();

}

private void refreshButton_click(object o, System.EventArgs e)

{

try

{

GetSecurityForWeb(drpSiteList.SelectedValue.ToString());

UpdateStopWatch(drpSiteList.SelectedValue.ToString());

Label1.Text = "<BR>Refreshing is done";

Label1.Text +="<BR>Fetching items lasted : " +Fetch.Elapsed.Minutes+ "m " + Fetch.Elapsed.Seconds + "s" + " Importing items lasted : " +Import.Elapsed.Minutes+ "m " + Import.Elapsed.Seconds + "s";

Label1.Text +="<BR><a href='Lists/Analyse/AllItems.aspx'>Click here</a> to view the grid";

}

catch(Exception error)

{

Label1.Text += "<BR>" + error.Message.ToString();

}

}

protected override void RenderWebPart(HtmlTextWriter output)

{

SPWeb web = SPControl.GetContextWeb(Context);

if (web.UserIsWebAdmin)

{

try

{

output.Write("<script language=javascript>");

output.Write("function UpdateImg"+nummer+"() {");

output.Write("var img = document.getElementById('taskpic"+nummer+"');");

output.Write("if (img != null) img.src = '_layouts/images/bigrotation2.gif';");

output.Write("return false;");

output.Write("}");

output.Write("</script>");

output.Write("<div id='waitmessage"+nummer+"' style='Z-INDEX:10; DISPLAY:none; WIDTH:100%; POSITION:relative; HEIGHT:100%'>");

output.Write("<table border='0' style='BORDER-COLLAPSE: collapse' width='100%' height='100%' cellspacing='1' bgcolor='#ffffff' bordercolor='#000000'>");

output.Write("<tr>");

output.Write("<td align='middle' bordercolor='#ffffff'>");

output.Write("<img id='taskpic"+nummer+"' src='_layouts/images/bigrotation2.gif'>");

output.Write("</td>");

output.Write(" </tr>");

output.Write(" </table>");

output.Write("</div>");

output.Write("Please select a site to display information from : ");

drpSiteList.RenderControl(output);

refreshButton.RenderControl(output);

Label1.RenderControl(output);

}

catch(Exception error)

{

output.Write(error.Message.ToString());

}

}

else

{

Label1.Text = "You don't have enough permissions to run this webpart";

}

}

To populate the dropdown with webs the copy/paste this following codechunk

private ArrayList GetSubWebs()

{

ArrayList lijst = new ArrayList();

SPWeb web = SPControl.GetContextWeb(Context);

Sites site = new Sites();

site.Title = web.Title.ToString();

site.Url = web.Url.ToString();

lijst.Add(site);

foreach (SPWeb _subweb in web.GetSubwebsForCurrentUser())

{

Sites subsite = new Sites();

subsite.Title = _subweb.Title.ToString();

subsite.Url = _subweb.Url.ToString();

lijst.Add(subsite);

}

return lijst;

}

With this class to store the urls and titles of the webs

using System;

namespace CrossSiteGroupWebPart

{

/// <summary>

/// Summary description for Sites.

/// </summary>

public class Sites

{

private string _url = string.Empty;

private string _title = string.Empty;

public string Url

{

get

{

return _url;

}

set

{

_url = value;

}

}

public string Title

{

get

{

return _title;

}

set

{

_title = value;

}

}

}

}

I will post the solution soonly so you can see for yourself how it all works :)