 |
|
If you have a question or suggestion, please contact me through Windows Live Messenger. My status:
 . If I am not online, please send me an e-mail. |
|
|
|
|
|
|
 |
|
|
|
|
|
|
|
|
5/22/2009For our e-office intranet I was working on a number of page layouts. In this page layout I wanted to use the out of the box Page Field Filter web part. After creating a new page using that page layout, the page crashed immediatly, showing the error message “An unexpected error has occurred”. After switching off customErrors in web.config, the error message was “The Hidden property cannot be set on Web Part 'g_8271d6f6_a902_4fa4_88ce_ca9ae1b0d463', since it is a standalone Web Part.”. Context filter web parts are not visible at runtime, the only show up when the page is in edit mode. The web parts are using the hidden property to hide themselves. The way this is done does not work when using the web part directly in a page layout, resulting in this error message. I decided to change the Page Column Filter web part I released on CodePlex, to make this work. In this web part I created a new override of the Hidden property. If there is a web part zone in which the web part is used, it behaves normally. If there is no web part zone, the property always returns false to prevent the web part from throwing the error message above. Here’s the code: [Browsable(false)]
public override bool Hidden
{
get
{
if (base.WebPartManager == null)
{
return base.Hidden;
}
if (this.Zone == null)
{
return false;
}
return !base.WebPartManager.DisplayMode.AllowPageDesign;
}
set
{
base.Hidden = value;
}
}
Because our web part now returns false when used in a page layout, we need another way to hide the web part at runtime. To do this I also created another override of the Visible property. Here is the code:
[Browsable(false)]
public override bool Visible
{
get
{
if (base.WebPartManager == null)
{
return base.Visible;
}
if (this.Zone != null)
{
return true;
}
return base.WebPartManager.DisplayMode.AllowPageDesign;
}
set
{
base.Visible = value;
}
}
Please note that this code snippet always returns true if there is a web part zone. If you do not do this, your web part will throw an error when the web part is used in a web part zone (by adding it to the page through the web part gallery).
Now our context filter web part works as expected when used as a normal web part and when used in a page layout. Together with Waldek, I will be at the Dutch DevDays next week. We will both be in the Ask the Experts area trying to answer your SharePoint questions. So if you have any, please stop by and give us a hard time :-). Hope to see you there! 
5/21/2009Today I was looking at the Board of Directors template, which is one of the Windows SharePoint Services 3.0 Application Templates (also known as the fabulous 40). This template contains an event calendar for board events. When you add a new board event to that list, a SharePoint Designer workflow creates a new Meeting minutes document. Another workflow adds a new discussion to the discussion board. The display form of the board event contains a number of DataForm web parts that show the content related to the board event, as is shown in the screenshot below: The list of discussion items shows the discussion item created by the SharePoint Designer “Create Discusion” workflow. The link however points to the details page (dispform.aspx) of the discussion item, instead of the page that shows the thread (flat.aspx or thread.aspx). Another problem is that this view always show 0 replies. This is because the discussion item is created using the “Create List Item” activity in the workflow. This way an item is added to the discussion board, but it is not a discussion. You can see that by navigating to the list and opening the item from the summary view. You are now also redirected to the dispform.aspx page and if you reply to the discussion, replies are added to the list as new items, instead of as a reply to the initial item. The screenshot below shows the issues: The idea of the workflow that creates a discussion item for every board event is nice, but it does not work properly. There are 2 ways to fix this. The nicest way is to create a new activity for SharePoint Designer workflows that creates a new discussion item and use that in de workflow. This blog post by Ricardo Costa shows how to do that. Another option is to create a workflow in Visual Studio that creates a new discussion item using SPUtility.CreateNewDiscussion. Then you replace the out of the box Create Discussion workflow in your template. In both cases you will need to create the discussion item and link that to the meeting event (using the Meeting lookup field). Here is the code how to do that: foreach (SPList list in workflowProperties.Web.Lists)
{
// Check if this list is a discussion board.
if (list.BaseTemplate == SPListTemplateType.DiscussionBoard)
{
// Start a new discussion on the title of the event.
SPListItem newDiscussion = SPUtility.CreateNewDiscussion(list.Items, workflowProperties.Item.Title);
// Update the lookup field to the event, if the field is available.
if (list.Fields.ContainsField("Meeting"))
{
newDiscussion["Meeting"] = workflowProperties.Item.ID;
}
// Update the discussion item.
newDiscussion.Update();
// Just add a new discussion thread to the first discussion board found.
break;
}
}
Last thing you will need to do is to fix the link in the dataview web part. Instead of dispform.aspx, the item needs to point to either the flat.aspx or the threaded.aspx page. Open the dispform.aspx page of the Calendar list.
Search in the XSLT of the Discussion web part for the <td> that renders the hyperlink to the dispform.aspx page using the ID of the item. Replace that <a> with this code:
<a href="../../Lists/Team%20Discussion/Threaded.aspx?RootFolder=Lists%2FTeam%20Discussion%2F{@Title}"><xsl:value-of select="@Title" /></a>
That’s it, now the discussion board in the Board of Directors template works as it should. 4/1/2009This post describes a dilemma I am currently having. I created a solution and I am not sure if it is the best way to do this. So I hope to get some input of you to make the design better. I created a SharePoint solution that adds some links to the QuickLaunch navigation in the MySite. To do this, I created a new feature scoped at the site collection level. The feature is stapled to the MySite site definition. The feature uses a custom feature receiver that creates the new navigation nodes when the feature is activated. Everything works nicely. One of the requirements is that it should be possible to completely remove the solution, including the items in the navigation. So when the solution is uninstalled, the QuickLaunch should no longer show the links. I implemented the FeatureDeactivating event in my feature receiver to do the cleanup. Again, this works as expected. When I deactivate the feature, the nodes in the navigation disappear. Implement FeatureUninstaling The big question here is when the feature gets deactivated. If you remove the WSP solution, the site collection feature is no longer available and it is no longer stapled to the MySite site definition, but it does not get deactivated in the existing MySites. I implemented the FeatureUninstalling event in my feature receiver. In this event I iterate through all web applications and through all site collections and for every site collection that has my feature activated, I deactivate the feature. Nice, problem solved! That works a bit too well This solution works well, but is a bit too succesful in cleaning up. To install/update/uninstall our solutions, I alway use the SharePoint Solution Installer. When I do a repair, the installer does not do a ‘upgradesolution’, but it does a full retract/delete followed by an install. This issue with this is that the FeatureUninstalling event is triggered in this process. The deactivates all my features and removes the MySite navigation. Which is exactly what I wanted, but not in this scenario. Because when I now upgrade (or repair) my solution, all existing MySites loose their navigation. The options So what are my options here? - Stop using the SharePoint Solution Installer
I could stop using the Installer tool and stop upgrading the solution by doing a ‘Repair’. Well, that is not really what I want. It is a nice and easy to use UI to roll out my solutions. And who’s going to stop my customers from doing it themselves after I send them just my WSP? - Create a cleanup tool
I could remove the cleanup process from the feature to a custom console/Windows application and run that after I uninstalled my package. This will work, but I don’t really like the idea. - Reactivate features upon installing
This is the option I am currently using. In the initial FeatureActivated event, I mark the site collection as running my feature by writing a new value to the property bag of the rootweb. In the FeatureUninstalling event, I deactivate my feature in every site collection that uses my feature, as described above. In the FeatureInstalled event, I again iterate through all site collections in all web applications in the farm and I re-activate the feature for all site collections that have my custom marker in the property bag of the rootweb. This solution works well, but the issue I have with this is that upgrading is starting to become a very heavy process in big environments. And after uninstalling, I still leave some footprint. Who is going to remove the custom property from the property bag? The reason I wrote this post is that I am interested to hear if I have missed an option. And I would also be interested to hear how you solved this. If you have ideas, please let me know by writing a comment, or by e-mailing me directly. Thanks! 3/29/2009A number of customers have asked us to change the behavior of the Close button in an announcement. The default behavior is that when the user clicks the Close button, he is redirected to the default view of the list. Unless the querystring contains the ‘Source’ parameter. The user is then redirected to the value of that parameter. The question was if it is possible to make the Close button behave like the Back button of the browser. This is usefull when you aggregate announcements to multple places in your site collection(s)’. In this post I will describe how we implemented this change. The basic idea is that we add a piece of JQuery script to the Dispform.aspx pages. This script changes the onclick of the button. To add this script to the pages, we add a custom control to the pages using the AdditionalPageHead control template. This is a custom user control that decides whether or not the script is added to the page. This way we only add the script to the pages that need it. Step 1 – Register the control template The first step is to create a new feature that will register our control: <?xml version="1.0" encoding="utf-8" ?>
<Feature Id="F88ED2E4-1318-4679-B0F4-38EECDB608F6"
Title="Close Button behavior."
Description="Adjust the behavior of the Close button on display pages."
Version="1.0.0.0"
Scope="Site"
xmlns="http://schemas.microsoft.com/sharepoint/">
<ElementManifests>
<ElementManifest Location="controls.xml" />
</ElementManifests>
</Feature>
The contents of the controls.xml file:
<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<Control
Id="AdditionalPageHead"
ControlSrc="~/_controltemplates/TST/CloseButton.ascx" />
</Elements>
Step 2 – Create the control template
Next thing to do is create the control that is loaded in the AdditionalPageHead. This control loads a custom control called ‘CloseButtonControl’:
<%@ Control Language="C#" ClassName="CloseButton" %>
<%@ Register TagPrefix="TST" Namespace="TST.SharePoint.CloseButton"
Assembly="TST.SharePoint.CloseButton, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b2defc4f610a0b97" %>
<TST:CloseButtonControl ID="CloseButtonControl1" ListTemplates="Announcements;Tasks" runat="server">
</TST:CloseButtonControl>
This custom user control has a property called ‘ListTemplates’. This is a semi-colon separated list of list template names that determines to which list templates the new behaviour is applied. In case we don’t want to implement this new behavior for all lists in the site collection, we can set the ListTemplates in our ASCX file. If we want the user to go back to the previous on every list item page, we set it to a ‘*’. This control property ‘ListTemplates’ in the ASCX file is not the best way to implement such configuration values. In a future post I will get back to this configuration topic.
Step 3 – Add the script to the page
Next thing to do is create the control that adds the script to the SharePoint pages that need it. The code snippet below show the code for this control. In the OnLoad our control checks if we are in a view item page (dispform.aspx). If that is the case, the template setting is validated. If the current request is the dispform page of one of the list templates that is setup in the ASCX file, the JQuery file and the script itself are added to the page. In this example the script will be added for Task and Announcement items.
namespace TST.SharePoint.CloseButton
{
public class CloseButtonControl : UserControl
{
private const string JQUERYREGISTRATION = "TST.SharePoint.CloseButton.JQuery";
private const string SCRIPTREGISTRATION = "TST.SharePoint.CloseButton.Script";
public string ListTemplates
{
get;
set;
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
if (String.IsNullOrEmpty(ListTemplates) ||
SPContext.Current.List == null ||
!Page.Request.Url.ToString().Contains(SPContext.Current.List.Forms[PAGETYPE.PAGE_DISPLAYFORM].ServerRelativeUrl))
{
return;
}
// Validate list template.
string[] templates = ListTemplates.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
foreach (string template in templates)
{
if (template=="*" ||
SPContext.Current.List.BaseTemplate.ToString() == template)
{
if (!Page.ClientScript.IsClientScriptIncludeRegistered(JQUERYREGISTRATION))
{
Page.ClientScript.RegisterClientScriptInclude(typeof(CloseButtonControl),
JQUERYREGISTRATION, "/_layouts/TST/jquery-1.3.2.min.js");
}
if (!Page.ClientScript.IsStartupScriptRegistered(SCRIPTREGISTRATION))
{
StringBuilder script = new StringBuilder();
script.Append("<script type=\"text/javascript\">");
script.Append("$(document).ready( function(){");
script.Append(" $(\"input[name$='GoBack']\").each(function() {");
script.Append(" var click = \"javascript:history.go(-1);\";");
script.Append(" this.onclick = new Function(click);");
script.Append(" });");
script.Append("}");
script.Append(");");
script.Append("</script>");
Page.ClientScript.RegisterStartupScript(typeof(CloseButtonControl),
SCRIPTREGISTRATION, script.ToString());
}
return;
}
}
}
}
}
The JQuery script itself is very easy. It is a selector that finds the Close button and registers a new click function. The reason for registering this JQuery script from a usercontrol, is that we do not want every page to load the JQuery js file and run the script. Otherwise the solution would have been easier. You can register the JQuery file and the script directly in the ASCX file and you’re done.
Step 4 – Create the solution package
Last thing to do is create the DDF and manifest.xml files to create a solution package. The WSP file created from the DDF file below contains all necessary files.
;
.OPTION EXPLICIT ; Generate errors
.Set CabinetNameTemplate=TST.SharePoint.CloseButton_1.0.0.0.wsp
.set DiskDirectoryTemplate=CDROM ; All cabinets go in a single directory
.Set CompressionType=MSZIP;** All files are compressed in cabinet files
.Set UniqueFiles="ON"
.Set Cabinet=on
.Set DiskDirectory1=
manifest.xml manifest.xml
..\bin\release\TST.SharePoint.CloseButton.dll
..\12HIVE\TEMPLATE\LAYOUTS\TST\jquery-1.3.2.min.js LAYOUTS\TST\jquery-1.3.2.min.js
..\12HIVE\TEMPLATE\CONTROLTEMPLATES\TST\CloseButton.ascx CONTROLTEMPLATES\TST\CloseButton.ascx
..\12HIVE\TEMPLATE\FEATURES\TST.SharePoint.CloseButton\feature.xml TST.SharePoint.CloseButton\feature.xml
..\12HIVE\TEMPLATE\FEATURES\TST.SharePoint.CloseButton\controls.xml TST.SharePoint.CloseButton\controls.xml
These files are deployed to SharePoint using this manifest file:
<?xml version="1.0" encoding="utf-8" ?>
<Solution ResetWebServer="TRUE" SolutionId="7EC65C73-E63F-433f-9695-B0DBD41B811D" xmlns="http://schemas.microsoft.com/sharepoint/">
<Assemblies>
<Assembly DeploymentTarget="WebApplication" Location="TST.SharePoint.CloseButton.dll">
</Assembly>
</Assemblies>
<TemplateFiles>
<TemplateFile Location="LAYOUTS\TST\jquery-1.3.2.min.js"/>
<TemplateFile Location="CONTROLTEMPLATES\TST\CloseButton.ascx"/>
</TemplateFiles>
<FeatureManifests>
<FeatureManifest Location="TST.SharePoint.CloseButton\feature.xml"/>
</FeatureManifests>
<CodeAccessSecurity>
<PolicyItem>
<PermissionSet class="NamedPermissionSet" version="1" Description="Allow access to TST.SharePoint.CloseButton">
<IPermission class="AspNetHostingPermission" version="1" Level="Minimal"/>
<IPermission class="SecurityPermission" version="1" Flags="Execution" />
<IPermission class="Microsoft.SharePoint.Security.SharePointPermission,
Microsoft.SharePoint.Security, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" version="1" Unrestricted="True" />
</PermissionSet>
<Assemblies>
<Assembly Name="TST.SharePoint.CloseButton" Version="1.0.0.0" PublicKeyBlob="0024000004800000blablabla_etc." />
</Assemblies>
</PolicyItem>
</CodeAccessSecurity>
</Solution>
Step 5 – Test
After installing and deploying the WSP package, you can test the solution by activating the site collection feature. By de-activating the feature, the Close buttons in the lists return to the out of the box SharePoint behavior. 3/23/2009In my current project one of the requirements was to add navigation to SharePoint wiki’s. We defined a number of rules that drive the navigation and that are not important for this post. In this post I will describe how I added the navigation controls to the page. Before I started I thought it was pretty easy. I started off creating a a custom navigation provider. I added the navigation controls to the out of the template page, which is wkpstd.aspx in the folder ‘12HIVE\TEMPLATE\DocumentTemplates’. I did the last step just on my development machine to test the navigation. Do not do this in your environment, because it is not supported! But for testing purposes of the navigation provider, it worked very nice. After I finished this, I planned on creating my custom version of wkpstd.aspx and changing the reference to this template page from code. Turns out that this is not possible. And of course I had just done the demo. Everybody liked it and I just said: ‘it is almost finished. I just need to move the custom navigation controls to a custom template and then we can use it’. The reference to the template page is stored in the property bag of the SPListItem, in a property called ‘vti_setuppath’. As Gary LaPointe documented in this blog post, you cannot update this property. This was also mentioned in the comments for this post by Mart Muller. So there is no way to change the template for existing wiki pages. I then started to look into the alternative that Mart mentions in his post. This option creates a new custom page to create a new wiki page. This custom page then needs to set the reference to our custom wiki template page. But while researching this option in Reflector, I found that the out of the box page is using some internal methods to add ghosted pages. And I think it is still very hard to get around the out of the box template wkpstd.aspx. It is hardcoded at such a deep level that I found it too difficult to change this to a custom aspx page. I decided to take a different approach and use a custom usercontrol and add that to the page using the AdditionalPageHead control template. This control loads a custom ASCX file and adds that to the placeholder in the wiki page that is loaded. This control template is activated by a web scoped feature. The elements manifest for the feature looks like this: <?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<Control
Id="AdditionalPageHead"
ControlSrc="~/_controltemplates/Custom/CustomWiki.ascx" />
</Elements>
The CustomWiki.ascx file is a custom UserControl that is stored in the CONTROLTEMPLATES folder. It does not contain other controls, it just contains the Control element that registers the inherited class in the code behind. In this class, I override the CreateChildControls:
public class CustomWiki: UserControl
{
protected override void CreateChildControls()
{
base.CreateChildControls();
if (SPContext.Current.ListItem != null &&
SPContext.Current.ListItem.Properties.ContainsKey("vti_setuppath") &&
SPContext.Current.ListItem.Properties["vti_setuppath"].ToString() == "DocumentTemplates\\wkpstd.aspx")
{
Control leftActions = GetControl(this.Page, "PlaceHolderLeftActions");
if (leftActions != null)
{
Control nav = LoadControl("wikinavigation.ascx");
leftActions.Controls.Add(nav);
Control recentChanges = GetControl(leftActions, "RecentChanges");
if (recentChanges != null)
{
leftActions.Controls.Remove(recentChanges);
}
}
}
}
private Control GetControl(Control ctrl, string controlName)
{
Control result = null;
if (string.Compare(ctrl.ID, controlName) == 0)
{
return ctrl;
}
foreach (Control c in ctrl.Controls)
{
result = GetControl(c, controlName);
if (result!=null)
{
return result;
}
}
return null;
}
}
The CreateChildControls checks if we are in a wiki page, by checking value of the vti_setuppath property of the current listitem. If this equals wkpst.aspx, we know we are in a wiki page. In that case, we use the function GetControl to find the placeholder to which we would like to add our navigation controls. In our case, this is called ‘PlaceHolderLeftActions’. If that placeholder is found, we load a new user control called ‘wikinavigation.ascx’ and add that to the controls collection of the placeholder. Our custom controls also hides the out of the box ‘Recent Changes’ control in the wiki page template.
I would prefer to have done it in a different way. But I still think it is a nice way to change the out of the box, hard coded, wiki template page. And the advantage of this technique is that when you de-activate the feature, the control template is no longer used, and the wiki pages behave link an out of the box SharePoint wiki page. 3/18/2009Two search related issues that we ran into in one of the projects I am currently working on. Case sensitive url Today we were indexing content of unix servers. The urls for these items are case sensitive. The content was correctly indexed, but after clicking the link, users were redirected to a “page not found” page. It turned out there has been a hotfix for this problem. You can find it in kb article 932619. This hotfix is included in SP1, or in a rollup package, because in our environment it was already available. We just had to change the registry setting (see section “How to enable the hotfixes”). After resetting the index and a full crawl, the unix url were correctly capitalized in the SharePoint search index.
Index not updated We recently upgraded to the Infrastructure update. After upgrading the search indexer stopped indexing content. Search was still available, but no content was indexed. After digging in the event log and the SharePoint log files, we came across event 6482:
Microsoft.Office.Server.Search.Administration.SearchServiceInstance (a7234003-d4d1-47a4-ae51-4bfce96be78a). Reason: Could not find file 'C:\WINDOWS\system32\drivers\etc\HOSTS'. After putting an empty HOSTS file in the folder mentioned above, the indexer started doing it’s job, and search was behaving as before. Strange.. 3/15/2009
In this post I have described why we created a custom security model for workspaces. When adding members to user groups, the people editor is initially disabled. Users first need to select one of the available groups. Then they can members from that group, using the Browse button. The screenshow below shows this process.
This makes it easier for users to select members and they only can select members that have permissions to that part of the site hierarchy. In this post I will describe how I created this.
Step 1 – The Add Users page
The first thing to create is a new ASPX page, called CustomAclInv.aspx. Initially this page is a copy of the out of the box AclInv.aspx page, that can be found in the Layouts folder in the 12 hive. The first thing to change in our to this file is the registration of our custom page handler class: <%@ Assembly Name="MyCompany.MyProject.Pages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1234567890123456"%>
<%@ Page Language="C#" Inherits="MyCompany.MyProject.Pages.CustomAclInvPage" MasterPageFile="~/_layouts/application.master" %>
Next we add the dropdown list that allows users to select a SharePoint group by changing the InputFormSection that contains the people editor:
<wssuc:InputFormSection Title="<%$Resources:wss,aclinv_Step1Title%>"
Description="First select a user group from one of the available groups. Then you will be able to select users from that group."
runat="server">
<Template_InputFormControls>
<wssuc:InputFormControl LabelText="Select one of the available usergroups." runat="server">
<Template_Control>
<asp:DropDownList runat="server" ID="selectGroup"></asp:DropDownList>
</Template_Control>
</wssuc:InputFormControl>
<wssuc:InputFormControl LabelText="<%$Resources:wss,aclinv_UserGroupsLabel%>" runat="server">
<Template_Control>
<wssawc:PeopleEditor
AllowEmpty="false"
ValidatorEnabled="true"
id="userPicker"
runat="server"
Enabled = "false"
SelectionSet="User"
PrincipalSource="UserInfoList"
/>
</Template_Control>
</wssuc:InputFormControl>
</Template_InputFormControls>
</wssuc:InputFormSection>
In this snippet I also made the changes to the People Editor. SelectionSet is set to ‘User’ and the PrincipalSource is changed to ‘UserInfoList’. Other changes I made to the CustomAclInv.aspx page are textual changes that are not relevant for this post. I also disabled the controls that allow users to set permissions directly. Warning: do not delete these controls, because then the postbacks of this page will result in an error.
Our custom ASPX is deployed in the LAYOUTS folder. Best practise is to deploy these custom ASPX pages to a custom sub folder in the Layouts folder. Because I am using the out of the box class to do the real work, I had to deploy it to the Layouts folder. Otherwise the redirects after clicking the OK/Cancel button will not work correctly.
Step 2 – The code behind
In the assembly we added a new class to handle the changes we made to the ASPX. This class inherits the out of the box AclInv page. Therefore you need a reference to the Microsoft.SharePoint.ApplicationPages assembly. In CreateChildControls the dropdown list is populated. We iterate through all permissions on the parent web. All SharePoint groups that at least have the ViewListItems permission and not have Full Control are added to the dropdown list.
namespace MyCompany.MyProject.Pages
{
public class CustomAclInvPage : AclInv
{
protected DropDownList selectGroup;
protected override void CreateChildControls()
{
base.CreateChildControls();
selectGroup.Items.Clear();
selectGroup.AutoPostBack = true;
selectGroup.SelectedIndexChanged += new EventHandler(selectGroup_SelectedIndexChanged);
selectGroup.Items.Add(new ListItem("-- Select a group --", string.Empty));
using (SPWeb parent = SPContext.Current.Web.ParentWeb)
{
// Test if the parent is assigned and the parent has unique permissions.
if (parent != null && parent.HasUniqueRoleAssignments)
{
// All groups that have specific permissions set, will be added to the dropdown list.
foreach (SPRoleAssignment assignment in parent.RoleAssignments)
{
if (assignment.Member is SPGroup)
{
bool valid = false;
// Add the group to the dropdown if it has permissions to open items.
// Exclude Full Control groups.
foreach (SPRoleDefinition role in assignment.RoleDefinitionBindings)
{
if ((role.BasePermissions & SPBasePermissions.ViewListItems) == SPBasePermissions.ViewListItems &&
role.BasePermissions!=SPBasePermissions.FullMask)
{
valid = true;
break;
}
}
if (valid)
{
selectGroup.Items.Add(new ListItem(assignment.Member.Name, assignment.Member.Name));
}
}
}
}
}
}
private void selectGroup_SelectedIndexChanged(object sender, EventArgs e)
{
if (string.IsNullOrEmpty(selectGroup.SelectedValue))
{
userPicker.Enabled = false;
userPicker.SharePointGroup = string.Empty;
}
else
{
userPicker.Enabled = true;
userPicker.SharePointGroup = selectGroup.SelectedValue;
}
}
}
}
In the SelectedIndexChanged event, the userPicker is enabled, depending on the availability of a user group. The SharePointGroup property of the people editor is set to the name of the selected user group. By doing this, the people editor changes its browse behavior. Instead of searching for new members, users can now select them from a list.
Step 3 – Changing the Add Users button
Next thing to do is to change the behavior of the ‘New’ and the ‘Add Users’ button in the toolbar of the People and Groups page.
These buttons must redirect the user to the new Add Users page that we just created. I first tried to do this by adding a new custom action to the menu and hiding the out of the box menu items. The problem was that HideCustomAction in a feature did not hide these items. I therefore decided to add a piece of javascript to the page to change the link to these pages. You can find the script below. It is a mixture of javascript and jQuery. The first bit is a jQuery selector that finds the ‘New’ link and replaces the link. The second bit does the same for the ‘Add Users’ link. I tried using a jQuery selector here also, but I did not succeed in finding ‘ie:menuitem’ elements using a selector.
This is script is added to the page by using a custom controltemplate. The script showed above, is stored in a ASCX file in the CONTROLTEMPLATES folder. A new feature registers the controltemplate for the ‘AdditionalPageHead’ control id.
<?xml version="1.0" encoding="utf-8" ?>
<Feature Id="F1701D3B-81C8-4FB4-8536-DE8AC7D93E92"
Title="Custom Menu Redirect Feature."
Description="Activates the custom Add Users page."
Version="1.0.0.0"
Scope="Site"
xmlns="http://schemas.microsoft.com/sharepoint/">
<ElementManifests>
<ElementManifest Location="controls.xml" />
</ElementManifests>
</Feature>
The content of controls.xml:
<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<Control
Id="AdditionalPageHead"
ControlSrc="~/_controltemplates/Custom/CustomMenuRedirects.ascx" />
</Elements>
The part of the solution is the one I am least happy with. Every page in the site collection that have the feature enabled has renders and runs this script. The only page that needs to render this script however, is the people.aspx page. If you know another solution to change these 2 links, I would love to hear from you.
Step 4 – Changing the Site Users web parts
As described in the previous part, our solution has a Participants page. This page has a number of ‘Site Users’ web parts. I created a custom web part that registers a piece of javascript in the page. This web part is also added to the page and when it runs, it does these 3 things:
- Change the Add Users link to redirect to the correct page using the groupid of the group that the user has selected in the web part properties.
- Add a link ‘Manage Users’. This basically is the same link as the out of the box ‘People and Groups’ link in teamsites.
- Add the ‘E-mail users’ link
The code below shows the code of this so called ‘FixMembersWebPart’ web part.
The can be downloaded from this zipfile. (for some reason I was not able to publish it directly)
‘JustAddMembersToGroups’ in this code snippets checks whether of not the feature for the custom ‘Add Users’ page is activated. If this feature is activated, the Site Users webpart will redirect the users to the custom page that was described above. If is is not activated, they are redirected to the out of the box aclinv.aspx page. This javascript was written before I knew about jQuery, now I probably would have written it using jQuery.
Step 5 – The e-mail users page
The custom e-mail users page that makes it easier for users to send the site members an e-mail is a copy of people.aspx, deployed to a custom folder in the Layouts folder. The changes are easy to do and therefore I just mention them:
- Stripped down toolbar menu
- All members are automatically selected when the page loads
- Textual changes
Conclusion
In this post I have described how we created the solution in the previous post. As you can see, it is pretty easy to build a custom user experience for adding users to SharePoint group. Making it easier for members to manage the membership of SharePoint groups, and making the security model for our project sites easier to understand. 3/10/2009I keep forgetting how to do this, so I decided to write it down. In SharePoint workflows created with Visual Studio you can easily write to the history list. This makes it easier to troubleshoot your workflows. Helper methods First I create 2 static helper methods that do the work for me. public static class Common
{
private static string SUCCESS = "Success";
private static string FAIL = "Failed";
public static void WriteSuccessToHistoryLog(SPWeb web, Guid workflow, string description)
{
TimeSpan ts = new TimeSpan();
SPWorkflow.CreateHistoryEvent(web, workflow, 0, web.CurrentUser, ts, SUCCESS, description, string.Empty);
}
public static void WriteFailToHistoryLog(SPWeb web, Guid workflow, string description)
{
TimeSpan ts = new TimeSpan();
SPWorkflow.CreateHistoryEvent(web, workflow, 0, web.CurrentUser, ts, FAIL, description, string.Empty);
}
}
Logging from the workflow itself
From the code-behind of the workflow, it is easy to call these methods. We first need to get the workflow activation properties, that have a reference to the context objects:
public Microsoft.SharePoint.Workflow.SPWorkflowActivationProperties _WorkflowProperties = new Microsoft.SharePoint.Workflow.SPWorkflowActivationProperties();
Using the properties object, we can call our log methods:
Common.WriteSuccessToHistoryLog(_WorkflowProperties.Web, WorkflowInstanceId, "Hello!");
Logging from a custom activity
When used from a custom activity, we need to do a bit more work to pass the context of the workflow to the activity. My custom activity has custom properties for the site in which the workflow runs and the ID of the workflow instance. I pass the WorkflowInstanceId specifically to the activity, because my activities and workflows are in separate assemblies. Here are the property definitions:
public static DependencyProperty InstanceIdProperty = DependencyProperty.Register("InstanceId", typeof(Guid), typeof(AddWeb));
[DescriptionAttribute("Workflow Instance Id")]
[CategoryAttribute("Workflow Properties")]
[BrowsableAttribute(true)]
[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
public Guid InstanceId
{
get { return ((Guid)(base.GetValue(AddWeb.InstanceIdProperty))); }
set { base.SetValue(AddWeb.InstanceIdProperty, value); }
}
public static DependencyProperty ListContextWebProperty = DependencyProperty.Register("ListContextWeb", typeof(SPWeb), typeof(AddWeb));
[DescriptionAttribute("ListContextWeb")]
[CategoryAttribute("Workflow Properties")]
[BrowsableAttribute(true)]
[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
public SPWeb ListContextWeb
{
get { return ((SPWeb)(base.GetValue(AddWeb.ListContextWebProperty))); }
set { base.SetValue(AddWeb.ListContextWebProperty, value); }
}
In this sample, AddWeb is the name of my custom activity.
Calling the logging methods is then done like this:
Common.WriteSuccessToHistoryLog(ListContextWeb, InstanceId, "Hello from an activity.");
The last thing to do is pass the correct parameters to pass the correct parameters to your activity. Open the workflow in designer mode, select your activity and select these values for the 2 properties:
When you now look at the workflow history of a specific workflow running on a listitem, the page will show you much more detailed information of what is happening in the workflow. 3/8/2009
I was looking at the Redirect method in the SPUtility class. Besides the url, this method also takes a parameter of type SPRedirectFlags. I was wondering when to use the values of this enumeration. The MSDN documentation does help a bit, but not very much. I asked on Twitter if anyone knew the meaning of these values. Within minutes, Anders answered and pointed me in the right direction. One of the examples of why I love Twitter!
The signature of the Redirect method is:
public static bool Redirect(string url, SPRedirectFlags flags, HttpContext context)
There is an overload that also takes the queryString as a parameter. The SPRedirectFlags enumeration has these values:
- CheckUrl
- Default
- DoNotEncodeUrl
- DoNotEndResponse
- RelativeToLayoutsPage
- RelativeToLocalizedLayoutsPage
- Static
- Trusted
- UseSource
Source url
If UseSource is part of the flags parameter of the Redirect method, the url to which the user is redirected will be read from the querystring of the original request (context parameter). The new url will be the value of one of these querystring parameters:
- Source
- NextUsing
- NextPage
If one of these parameters has a value, the new url will be validated. Validation is done by the IsUrlSafeForRedirect method of the Request property of the current SPWeb. The url in the querystring of the original request needs to be a relative url. See the samples below.
If this url is not valid, or the UseSource parameter resulted in an empty string, the url parameter that was originally passed will be used.
Static url
If Static is part of the flags parameter, the url is considered relative. Depending on the presence of RelativeToLayoutsPage in the flags parameter, the url is relative to the ´_layouts´. If this enumeration value is present, SharePoint checks the flags parameter for the presence of RelativeToLocalizedLayoutsPage. If this is present, a new absolute url to the localized ‘_layouts’ folder is constructed. If not, the url is constructed to the root of the ‘_layouts’ folder. The layouts url is the url of the current SPWeb, followed by ‘_layouts’ and the Language of the current SPWeb. If constructing this url fails for whatever reason, the url will be the url of the current SPSite. If required, the value of SPGlobal.ServerCulture.LCID is added to the url.
Absolute url
If Static is NOT part of the flags parameter, the user will be redirected to the value of the url parameter, after validating the url. If Trusted is part of the flags parameter, the url is always valid. If Trusted is not available, it depends on the outcome of the IsUrlSafeForRedirect method of the Request property of the current SPWeb whether or not the url is valid.
Encoding
The last step is before the user is redirected is the encoding. If DoNotEncodeUrl is NOT present in the flags attribute, the url is first encoded using SPHttpUtility.UrlPathEncode.
Samples
Below you will find a number of samples. Each sample starts with the sample code for SPUtility.Redirect. This code is tested in a web part. The 3 lines below that sample show the results of calling the redirect. The first column contains the page url that contains the web part. The second column contains the result of the redirect.
SPUtility.Redirect(http://newsite, SPRedirectFlags.Default, HttpContext.Current);
SPUtility.Redirect("/news", SPRedirectFlags.Default, HttpContext.Current);
SPUtility.Redirect(http://newsite, SPRedirectFlags.Static, HttpContext.Current);
SPUtility.Redirect(http://newsite, SPRedirectFlags.UseSource, HttpContext.Current);
SPUtility.Redirect(http://newsite, SPRedirectFlags.UseSource | SPRedirectFlags.Trusted, HttpContext.Current);
SPUtility.Redirect(http://newsite, SPRedirectFlags.Trusted, HttpContext.Current);
SPUtility.Redirect("settings.aspx", SPRedirectFlags.Static | SPRedirectFlags.RelativeToLayoutsPage, HttpContext.Current);
SPUtility.Redirect("images/approve.gif", SPRedirectFlags.Static | SPRedirectFlags.RelativeToLayoutsPage | SPRedirectFlags.RelativeToLocalizedLayoutsPage, HttpContext.Current);
There are a few members of the enumeration that I did not describe, because I was not able to find out how these are used.
|
|
|
| | | |