January 2008 Entries

I mentioned in another post some of the issues with solutions built using AdxStudio version 5.3 when based on AD/AM instead of active directory, one of which was the forgotten password control not working. Apparently this has been fixed in version 6 but this wasn't much help to me as the client didn't want to upgrade at the moment thus I was forced to come up with an alternative solution and this article covers the steps involved. My aspx page contains a textbox for the username and a button for the user to submit to receive a new password. Then in the code behind in my OnClick method I search AD/AM for the user, generate a random password, set the password, and email the user.

Obviously if I’m going to change the user's password I first need to find the Directory Entry for that user in AD/AM. There are lots of articles on how to do this one I found very helpful was www.codeproject.com/KB/system/everythingInAD.aspx. In the example below I am creating an instance of a Directory Entry passing in the user whom I am about to change the password off, username and password details that will be used to connect to AD/AM

            string strUser = username
           string strPwd = password
            DirectoryEntry entry = new DirectoryEntry(userDN, strUser, strPwd);

Next I generated a random password for the user and again there are lots of articles for this so I won't go into it here. This next section is the area that caused me some problems.

            string strPassword = your new password
            entry.RefreshCache();

            int intPortNumber = port number of the AD/AM instance

            entry.Invoke("SetOption",
            new object[] { 6, intPortNumber });

             entry.Invoke("SetOption",
             new object[] { 7, 1 });

             entry.Invoke("SetPassword",
             new object[] { strPassword });

             entry.CommitChanges();

Now I have changed the password of my user all I have to do is let them that it has been changed and then email them with the new password. Again this is straight forward enough and there are plenty of articles on how to do this. I have added a few links that helped me below:

en.csharp-online.net/User_Management_with_Active_Directory
msdn2.microsoft.com/en-us/library/dx0d151f(VS.71).aspx
blackfalconsoftware.wordpress.com/2007/04/24/tools-code-working-with-active-directory-in-c/


Bookmark with :
Digg It! DZone StumbleUpon Technorati Reddit Del.icio.us Newsvine Furl Blinklist

A common task when you are setting up a new CMS system is to migrate content from the old system to the new one. Thankfully AdxStudio have provided the bases for the code to do this, docs.adxstudio.com/Default.aspx. I found this article very useful as it gives you the bases from which to get a simple importer tool up and running very quickly, however there is one problem with this code. If you use this code and start to import content into the CMS it will appear to be working ok but if you open one of the pages, save and close it then re-open it you should notice that the content heading and copy will have gone. This is because of the concept in AdxStudio that there can be multiple content sections, basically when you import the content you need to assign to it a section number. If you don't and you run the code as is then if you open up the imported document and view the xml you should see the section number has not been filled in, see below.



This is why if you were to save and close then open the document you will find the content will have gone. The problem in the importer code is in the section below particularly focusing on 'EnglishSectionHeading' and 'EnglishSectionCopy'.  We need to have some way of telling the system what section the heading and the copy belong to.

            docdata.Add("EnglishLongTitle", longtitle);

            docdata.Add("EnglishKeywords", keywords);

            docdata.Add("State", state);          

            docdata.Add("EnglishSectionHeading", section_heading);  

            docdata.Add("EnglishSectionCopy", section_copy);

To overcome this issue I added to following code after you have used the document map to build the document xml. Basically I'm finding the section number and replacing '%I%' with '1' and this solves the problem.

                XmlNode xmlSectionNode = documentXml.SelectSingleNode("/adxDocument/Content/Section/@Number");
                if (xmlSectionNode != null)
                {
                    xmlSectionNode.InnerText = "1";
                }

This is fine except it involves adding those extra lines of code when it is something that you would expect should be handled by AdxStudio. The interesting thing is there is another way but it is not documented anyway and I only discovered this from discussions with AdxStudio relating to another issues. You can alter the code above to the following then you don't need to alter the document xml.

            docdata.Add("EnglishSectionHeading;1", section_heading); 
            docdata.Add("EnglishSectionCopy;1", section_copy);


Problem solved the content will now be saved and it will recognize that it applies to section number one


Bookmark with :
Digg It! DZone StumbleUpon Technorati Reddit Del.icio.us Newsvine Furl Blinklist

I have recently been involved in a project using AdxStudio version 5.3 based on AD/AM and have found that while it is supposed to be supported you may find there are certain bugs that can cause you a few problems. So to save you some time here are the ones I’ve encountered and what you can do to get around them.

  • Setting document state permissions - There is a bug in one of the administration pages that means you can't restrict the permissions of any states to individual user/groups. There is a solution available but you have to contact AdxStudio for the fix. 
  • Setting notes on a user - One of the nice features about AdxStudio is you can store notes about a user such as comments they made, when you contacted them etc but unfortunately this doesn't work in AD/AM. There is a fix available so if this is a problem you're having contact AdxStudio.
  • Using the standard .Net forgotten password control - Basically if you are using the standard .Net forgotten password control and your site is based on AD/AM it will not work. Again i have contacted AdxStudio but they are not releasing a fix for this so I was forced to write my own, which I will make available shortly.

AdxStudio have assured me that most of these issues have been fixed in the latest version but obviously this isn't much help to those of us on 5.3. If you come across any more then let me know and I’ll add them to the list


Bookmark with :
Digg It! DZone StumbleUpon Technorati Reddit Del.icio.us Newsvine Furl Blinklist

One issue I have noticed with AdxStuidio in version 5.3 is the format of the search results if you are using their built in control. The control works by only returning x number of rows at a time depending on what value you set on the control. So for example if you set it to return 10 results per page it get the results and bring back the first 10 results only. This is fine if you don't have any secure pages on the site but what happens if some of the pages/content is restricted to certain users. The search will go away and bring back the first 10 results without considering if the user has access to see these results. The problem is if the user doesn't then have access to see some of the result you can end up with a situation where the figures are tell you there is 11 results but you only get 9 displayed because you don't have access to the other 2, see screenshots below.

Page 1 of the search results
Search Results Page 1

Page 2 of the search results


Search Results Page 2

You can see from the screenshots it is telling me there are 11 results returned by the search but then I only see 9 results. In addition the number of the results is wrong because it goes from result number 8 on the first page then on the second page it jumps straight to result 11. While you can remove the part in the XSL that displays the result information this is far some ideal. To overcome this problem I created a custom control that inherited from the AdxStudio search control this way I could get access to some of the private methods and only return the results that are appropriate. The general concept behind this post is to use a custom control to capture the results but then to filter these and bind them to my own gridview.

In this post I’m only going to cover my steps after creating a custom control but I may return to the previous steps at a later date. My first step was to add the custom control onto my aspx page setting the parameters as before except this time I have set my ResultsPerPage to 500. This is so that when the page is first hit the custom control should hold all the results returned by the search instead of only five or ten.

In the code behind I can now get access to all the search results and place them into a collection by using the following:

            results = new List<Adxstudio.Cms.DocumentSearchResult>();
            Adxstudio.Cms.DocumentSearchResultCollection collection = SearchResults.returnCollection();
       
I now have an empty generic list and a DocumentSearchResultCollection object that holds all the results including those that the user doesn't have access to. Next I want to loop through the DocumentSearchResultCollection and add all items that the user has access to into my generic list. First I perform the usual checks to make sure the individual result object is not null and that the document is valid. Next I access the GetData method and this is where the advantage of creating the custom control comes in because this method is private in the standard control hence I could see it had data when I debugged the page but previously couldn’t use this data. This returns an xml document of results that the user has access to so I can use this and see if any of the document guid’s in it match the current result. If so then the user has permission to see the result and it gets added to the generic list.

foreach (Adxstudio.Cms.DocumentSearchResult result in collection)
            {
                   //Check the results has a value
                if (result != null)
                {
                   //Check the results has a valid document. This may not be the case if it has not been published to the site i.e. it
                    // has only been saved in a Draft state
                    if (result.GetDocument() != null)
                    {
                         //Next use the GetData method to return the xml of all results that the user has access to see.
                        XPathNavigator xmlDocument = SearchResults.GetData().CreateNavigator();
                        xmlDocument.MoveToRoot();
                        xmlDocument.MoveToFirstChild();
                        // Loop through each of the valid documents in the  xml
                        do
                        {
                            if (xmlDocument.NodeType == XPathNodeType.Element)
                            {
                                if (xmlDocument.HasChildren == true)
                                {
                                    xmlDocument.MoveToFirstChild();
                                    do
                                    {
                                        if (xmlDocument.GetAttribute("DocGuid", xmlDocument.NamespaceURI) != null)
                                        {
                                            if (xmlDocument.GetAttribute("DocGuid", xmlDocument.NamespaceURI).ToString() != "")
                                            {
                                                // Check to see if the DocGuid for the result object is the same as the any of the ones in the
                                                 // xml and if so the user has access to see this search result so add it to the list
                                                if (result.GetDocument().Guid.ToString().Equals(xmlDocument.GetAttribute("DocGuid", xmlDocument.NamespaceURI).ToString()))
                                                {
                                        results.Add(result);
                                    }
                                }
                            }
                                    } while (xmlDocument.MoveToNext());
                                }
                            }
                           
                        } while (xmlDocument.MoveToNext());                      
                    }
                }
            }

After this we then have a list of all search results that the user has permission to see so we want to bind that to our own grid view.

            if (results.Count != 0)
            {
                Grd_Results.DataSource = results;
                Grd_Results.DataBind();
            }
            else
            {
                LblMessage.Visible = true;
                LblMessage.Text = "Sorry no results have been returned please try again";
            }

We now have all results bound to our gridview so the next steps are to select the information you want to display and style your gridview as appropriate but again I will leave that for just now. If this is something you are having trouble with then leave a comment and I’ll try and help. The final thing you must do is to ensure that you have set your custom search control to be invisible as other wise you will get two sets of results.


Bookmark with :
Digg It! DZone StumbleUpon Technorati Reddit Del.icio.us Newsvine Furl Blinklist

During a recent project one of the requirements was for a user to be able to search for people based on their last name. To achieve this I put a content editor web part on one of the pages and wrote some html that added links to the search centre appending the search criteria on the URL. The problem with this was the MOSS people search doesn't allow you to easily sort by name. Initially I thought I could append the XSL and use this to sort the results, however this only sorts the results returned on that page. For example if you have 100 results and you are displaying 50 results per page, this is the maximum, then  the sorting will only apply to the first 50 results. At first I thought there would be another way around this as it seemed like a logical thing to be able to do but after much investigation I learned that the best option available was to write a custom page that would pull out all profiles,  sort them and display them my self.

I have broken the tasks required to achieve this into the following steps.

  1. Determine the sort criteria
  2. Connect to the SharePoint site
  3. Execute custom code with elevated permissions
  4. Reconnect to the SharePoint site
  5. Retrieve a list of users
  6. Sort the Users
  7. Bind them to Gridview
  8. Give users access

I will now cover each of these in turn:

Determine the sort criteria

The first step is to determine what the user is trying to search for. As I mentioned earlier in the article the users access this page by clicking on a link in a custom web part that points them to my page passing the search parameters so I need to retrieve this information and assign it to a variable.

                strProfileType = Request.QueryString["ProfileType"];
                strLastName = Request.QueryString["LastName"];

Connect to the SharePoint site

As I have created a separate page out with SharePoint the first step I had to perform was to connect to the site using the credentials of the user accessing the page.

                SPWeb theWeb = new SPSite(strURL).OpenWeb();
                SPUser currentUser = theWeb.CurrentUser;

                using (SPWeb webInUserContext = SPContext.Current.Web)
                {
                    // get current web and site guids from the current context
                    webGuid = webInUserContext.ID;
                    siteGuid = webInUserContext.Site.ID;

                    SPSecurity.CodeToRunElevated elevatedGetUsers = new SPSecurity.CodeToRunElevated(GetUsers);
                    SPSecurity.RunWithElevatedPrivileges(elevatedGetUsers);
                }

Execute custom code with elevated permissions

This section is import as if you don't include it you will find that when you try and loop through the profiles returned by the profile manager in the next section it will give you an error. See code above for example


Reconnect to the SharePoint site

In the GetUser method we now have to re-connect to the site as the impersonated user, which should now have permission to enumerate through the profiles. Otherwise the page would still think it was connected as the user trying to access the page and will say you don't have access.


             // get the site in this context
            SPSite site = new SPSite(siteGuid);
           
            // get the web in this context
            SPWeb web = site.OpenWeb(webGuid);           
            SPSite oPortalSite = new SPSite(strURL);
            ServerContext oContext = ServerContext.GetContext(site);

Retrieve a list of users

My first step in this section was to create a structure to hold the profile information so I could make use of it later on. For this I decided to use a generic list based on the SharePoint UserProfile class this way I can make use of the built in methods to access the information I am interested in. Next I created an instance of the profile manager class supplying the context obtain from above and looped through the list of profiles adding each one to my generic list. In this example I have added some checks to restrict the number of users returned as I am only interested in users whose last name starts with the supplied criteria but you can quite easily change this for your purposes.

            List<UserProfile> oPeopleList = new List<UserProfile>();

            UserProfileManager profileManager = new UserProfileManager(oContext, true);

            strMySiteUrlHost = profileManager.MySiteHostUrl;

            foreach (UserProfile profile in profileManager)
            {             
                if (strLastName != null)
                {
                    if (profile["lastname"].Value != null)
                    {
                        if (profile["lastname"].Value.ToString().ToLower().StartsWith(strLastName.ToLower()))
                        {
                            oPeopleList.Add(profile);
                        }
                    }
                }
            }

Sort the Users

Now I have my list of users I can now perform the sort and in this example I have done so by creating a comparison delegate, which I can use to overwrite the existing sort method. It takes two user profile objects and compares the last name values to determine the order they should be in. Now that I have this generic comparison method I can pass this to the sort method on my generic user profile list and this will sort it by last name.

        /*Comparison delegate used for sorting*/
        static int LastNameComparision(UserProfile x, UserProfile y)
        {
            return x["lastname"].Value.ToString().CompareTo(y["lastname"].Value.ToString());
        }

        oPeopleList.Sort(LastNameComparision);

Bind them to Gridview

The second last step I will cover in this blog was to bind my generic list to my gridview. This is doesn't require much explanation as I’m sure most people will have done this before. My first was to perform a simple check to establish if there are any results, if so I perform the sort as described above then I bind the list to the gridview.

            if (oPeopleList.Count > 0)
            {
                oPeopleList.Sort(LastNameComparision);
                grd_member_search_results.DataSource = oPeopleList;
                grd_member_search_results.DataBind();
            }
            else
            {
                LblError.Visible = true;
                LblError.Text = "Sorry there are no results returned. Please try again";
            }
        
Obviously since we are binding the datasource to the gridview in the code behind you will still need to alter the gridview to pull out the columns you are interested in and perform some formatting to the look and feel.

Give users access

There is one final step that we must take in order for users to be able to access this information and that is to give the network service account 'Manage User Profiles' and ‘Use Personal Features’ permission in the Shared Service Provider. To do this open central admin, click on the shared service provider, select manage permissions then add the user and give them the above permissions. This is key as otherwise you will get an access denied error on the page. I must thank Edin Kapic for this as I spent a long time trying to identify what the error was before finding his blog. You can find more information on this here edinkapic.blogspot.com/2007/08/enumerating-user-profiles.html
Conclusions

These are the basic steps needed to create a new page to perform custom people searching, however you may need to custom this for your own purposes. I hope this was helpful


Bookmark with :
Digg It! DZone StumbleUpon Technorati Reddit Del.icio.us Newsvine Furl Blinklist