Wednesday, December 12, 2012

SharePoint 2010 - Alternate Access Mappings, Fully Qualified Domain Names, and Access Denied Errors

Back in the good old days of SharePoint Portal Server 2003 and Microsoft Office SharePoint Server 2007, Alternate Access Mappings (AAM) were easy. All you did was add a binding to your IIS web site, add a mapping in Central Administration and you were good to go. Not so in SharePoint 2010. The new Claims Based Authentication structure changes the ballgame considerably. Now, Zones are much more important, and because everything is a claim now, the ability to change authentication methods in a different zone is much more complex. From a SharePoint Architecture point of view. From an Administration point of view, you just need to know a couple of gotchas to avoid getting tripped up.

Let's start with a fairly common situation. Your company uses SharePoint as its intranet. You only have one domain, so you can keep things simple on the DNS host names. You call your site SP2010. Then, for what ever reason, you need to make your site resolve to the fully qualified domain name (FQDN), SP2010.MyCompany.com. Easy enough, right? First you attempt to hit the site with the FQDN. You get the IIS 7 Welcome screen, or a 404 error.
You check the Application log on your SharePoint server and find an error that says:
A request was made for a URL http://SP2010.MyCompany.com which has not been configured in Alternate Access Mappings. Some links may point to the Alternate Access URL for the default zone http://SP2010. Review the Alternate Access mappings for this Web application at http://CENTRALADMINISTRATIONADDRESS/_admin/AlternateUrlCollections.aspx and consider adding http://SP2010.MyCompany.com as a Public Alternate Access URL if it will be used frequently.
Ok... Awesome.

You go to Central Administration, click on System Settings, and then Configure alternate access mappings, and are greeted with the AAM page.

Things are a little confusing here. To be honest, this page is just plan awful, especially if you have never made an AAM in SharePont 2010. After you know a little about what is going on, it makes more sense, but at first look... Terrible.
There are several buttons across the top allowing you to edit Public URLs and adding internal URLs and Mapping to External Resources. Don't worry about these for now.

The first thing we need to do is to change the view so that we only see the URLs for the Web Application that we are working with. On the right side of the screen, use the drop box to change the view from "Show All" to "SP2010." You click on the drop box and click the button that appears. A selection screen shows up, and you can pick the AAM collection that you will be working with. AAM collections are named according to the Web Application that contains them, so we click on SharePoint - 80, the name of our Web Application. Straightforward right? Ha! At least they give us the default URL so that we know a little bit about what is going on.

Now the AAM screen is back with just the URLs we are worried about.

What to do? First things first... We click on Edit Public URLs. (Disclaimer: After I get done with all of this, you are going to ask why I do my change this way, an not just add an Internal URL. You could. I am configuring this way to demonstrate the differences in zones and use the most confusing pages so that the explanation is clear, and can be used in multiple situations. It also looks a lot cleaner to have your FQDN as the public URL. ;-P)
If the AAM page was bad this page is much much worse.

What now??? It looks like we could just add a new URL here and everything will be cool right? No. Microsoft should have had some sort of validation or something here, because you can't just add a URL for a zone. You have to create the zone first. And... zones are not created in this area. If you want to create a new zone, you have to extend your Web Application in to a new IIS web site. If you add a URL in to one of the zone boxes provided, and try to hit the site, SharePoint and IIS don't know what to do with the traffic. If you are using Host Headers, you are likely to get the IIS 7 Welcome screen. If you are using ports or individual IP addresses, you are likely to get 404 or access denied errors. All this page does is tell SharePoint that traffic for a specific zone will come in over a specific URL. Nothing else.
Right now, we only have one zone defined for this Web Application, the Default zone. So, we can adjust that one. Change the default URL to have the FQDN address, and click Save. Remember that NO validation, other than a check to see if URL that you have saved matches another URL in any collection, is done on these URLs. SharePoint doesn't add anything to IIS in terms of bindings. No host headers are added. Your URL could be completely bogus, and SharePoint won't care.


Now we can do something that is actually pretty cool. In SharePoint 2010, you can have a many to one relationship with internal URLs to a single public URL. So, really you can define any URL you want for your SharePoint sites. As long as you add them to IIS and DNS. More on that later. For now, click on Add Internal URLs.

Here we get a new and somewhat less confusing page, but Microsoft gives us enough ambiguity and lack of validation to hang ourselves with.


Enter in any URL you wish. In our case we want the short URL of http://SP2010. The area that can get us in to trouble, again, is the Zone drop box. Remember we only have one zone defined, the Default zone. So even though you can change the zone, if you haven't extended your web application to create that zone, DON'T CHANGE it from Default. Click save and you go back to the AAM screen, provided that the URL you typed isn't in use by another Web Application.


You now see that we have definitions for both URLs, associated with the same Public URL. This is all we need to do to define our AAM. There is a bit of housekeeping you need to do though...

What we have just done is a SharePoint specific activity. It changes how SharePoint routs its own traffic and how SharePoint handles its own authentication. What we did does NOT change how DNS or IIS works. Those are separate process. If your URLs are not registered in DNS you will need to contact your DNS administrator to add them for you. If you are on your own for DNS work, and you don't know how to add a Host record (also called an "A" record), go here: DNS How To from Technet

For adding a host header in IIS go here: How to add a Host Header binding from Technet

If you have DNS, IIS, and AAM all configured correctly you now have your site set up to use both the short host name and FQDN for SharePoint. You also know some more about SharePoint zones, and how you can very quickly, and easily, add a URL to a SharePoint site.

Saturday, November 10, 2012

How Obamacare Will Help You

By YOU I mean if you have a few years of experience in the IT world. If you are brand new to the game... Sorry, but you are going to have a hell of a time just finding a job. What we will see now that Obamacare is a certainty, is that many companies are going to be laying off people. Companies, especially small business, especially will be working hard to shed workers off their roles to reduce their costs. This is bad. So, how can I work this to my advantage? Well, companies will still need IT work to be done. If they intend to stay in business, they will need MORE IT to automate processes that normally would be filled by workers. This means things like collaboration solutions, workflows, and administrative scripting will become very hot. Since business can't hire FTEs to do this work, who will do it? Contractors, consultants, and other temp workers can fill the personnel void without having to hire them full time. BUT, since most consulting agencies are, in fact, small business, it will be difficult to find a position at an actual consulting company. So how does this help me? Enter independent consulting. With a little leg and research work, you can set yourself up with your own company, with one employee, you. If you do the leg work before 2013, you can get ahead of the game and be ready if you are laid off. Companies will be looking for consultants, and you will be set up and ready to go. Now, working as an independent consultant means that you will have to pay for your own... everything. Insurance, taxes, social security, blah blah blah. You will have to figure out how much to charge per hour so that you can turn a profit. In this case profit means personal income, so remember that you are negotiating for bread on the table. HOWEVER, it is important that you keep your income below $250,000 after that amount you will really take it in the shorts from the tax man. Good luck in the coming months. They won't be easy for any of us. IT is seen as overhead for many business, so you can guarantee if cuts are to be made it will be with us.

Monday, October 1, 2012

Windows 2012 / 8 HID Service and Fancy Keyboards

After a catastrophic crash of my Windows 2008 R2 server, read I screwed up a BIOS update *sigh*,  I migrated my OS to Windows 2012.  The install of the Desktop experience was fairly straight forward, very similar to the procedure for Windows 2008, the only gotcha involved was finding the feature (it's under the User Interfaces and Infrastructure heading).

The big gotcha so far has been with my fancy keyboard. I have a Microsoft Natural Ergonomic Keyboard.  It is awesome.  Not only is it nice on the wrists, but it has handy dandy hot buttons that control volume, mute, launches the calculator, and other programmable buttons.  They are really handy.  BUT they didn't work after I migrated.

Everything I checked seemed to be working well, correct drivers were installed, nothing funny going on in the  event logs, the buttons just didn't work.

I then looked a bit harder at what showed up in the Device Manager:

HID keyboard...  Hummm....  There was a service in Windows 2008 that was HID.  In Windows 2012 the service is, Human Interface Device Access service.


As soon as I started that guy, all of the little buttons started to work.  An easy fix, but a necessary one if you have anything on Windows 2012 that has extras, you will need to start this service.  What is the service?  From Microsoft:
Enables generic input access to Human Interface Devices (HID), which activates and maintains the use of predefined hot buttons on keyboards, remote controls, and other multimedia devices. If this service is stopped, hot buttons controlled by this service will no longer function. If this service is disabled, any services that explicitly depend on it will fail to start.

So... The default in Windows 2012 is that this service is set to Manual(Trigger Start) so if your device does not have software to trigger this service, like my you will need to start it manually. I set my service to start automatically, so that I won't loose my buttons when I restart the server.

Monday, September 24, 2012

Copying Permissions From One List To Another List... In Separate Site Collections

My client has a aggregate list that is fed from several lists in many different site collections.  This list is populated by select fields from the feeder lists by an event receiver that fires when a list item's approval status is set to Approved.

The big gotcha here is that permissions and groups are handled at the Site Collection level.  So they do NOT transfer between the site collections.  This is not very intuitive, because...  well...  The permission levels all sound, look, and act the same.   BUT they are all exist in a separate context.  So, you need to do a little bit of code magic to make it happen.

Here we start the code.




private void ConfigurePermissions(SPListItem targetListItem, SPListItem sourceListItem) {
            SPSecurity.RunWithElevatedPrivileges(delegate() {
                using (SPSite sourceSite = new SPSite(sourceListItem.Web.Site.ID)) {
                    using (SPWeb sourceWeb = sourceSite.OpenWeb(sourceListItem.Web.ID)) {
                        sourceWeb.AllowUnsafeUpdates = true;
                        if (sourceListItem.HasUniqueRoleAssignments && sourceListItem.RoleAssignments != targetListItem.RoleAssignments) {
                            SPRoleAssignmentCollection sourceRoles = sourceListItem.RoleAssignments;
                            PropogatePermissions(sourceRoles, targetListItem);
                            targetListItem.Update();
                        } else if (sourceListItem.ParentList.HasUniqueRoleAssignments && sourceListItem.ParentList.RoleAssignments != targetListItem.RoleAssignments) {
                            SPRoleAssignmentCollection sourceRoles = sourceListItem.ParentList.RoleAssignments;
                            PropogatePermissions(sourceRoles, targetListItem);
                            targetListItem.Update();
                        }
                        sourceWeb.AllowUnsafeUpdates = false;
                    }
                }
            });
        }




First, you see that we are wrapping all of the code in a SPSecurity.RunWithElevatedPrivileges delegate.  You need to do this, otherwise the code would run as the user, and that user may not have permissions to execute everything that needs to happen.
Next, you have the standard "using" statements.  Even though the SPListItem object has a reference to the parent SPWeb object, and through that, the parent SPSite object, the SPListItem was instantiated under the user context, and therefore we need to create new objects under the elevated account.  The good news is that we can create the objects using the IDs contained in the SPListItem object, making it very easy and very safe to instantiate the correct site and web.
You'll notice that I have two places that a SPRoleAssignmentCollection could have come from. This is if the list item inherits its permissions or if it has unique permissions.

Now we get in to the meat of the code. I separated the code out in to two methods to help ease readability and debugging. It also made it easier to pass in the correct SPRoleAssignmentCollection, be it from the SPList or the SPListItem

private void PropogatePermissions(SPRoleAssignmentCollection sourceRoles, SPListItem targetListItem) {
            SPSecurity.RunWithElevatedPrivileges(delegate() {
                using (SPSite site = new SPSite(targetListItem.Web.Site.ID)) {
                    using (SPWeb web = site.OpenWeb(targetListItem.Web.ID)) {
                        if (sourceRoles != null) {
                            if (targetListItem.HasUniqueRoleAssignments) {
                                for (int i = 0; i < targetListItem.RoleAssignments.Count; i++) {
                                    targetListItem.RoleAssignments.Remove(i);
                                }
                            } else {
                                targetListItem.BreakRoleInheritance(false);
                            }


I do a check to make sure that the source SPRoleAssignmentCollection has some data.  This is nothing more than a data validation check.  If the object is null there is no reason to go on and face all of the exceptions right?

Next, I check to see if the target SPListItem is inheriting from the parent list, or if it has unique permissions of its own.  The handy dandy HasUniqueRoleAssignments bool property makes this a snap.  If the SPListItem is not inheriting, we simply call the BreakRoleInheritance method passing in false as the property.  This bool property tells the method to copy the existing inherited permissions or to start fresh.  Since we are going to replace the permissions with the permissions from the other list, we pass "false."
If the item already has unique permissions, we need to remove them.
Permissions, weather it be for the list itself or the list item, are kept as SPRoleAssignments in the object's SPRoleAssignmentCollection.  Much like the SPWebCollection object that keep references to all of the SPWebs in a SPSite object, so it goes with the SPRoleAssignmentCollection and SPRoleAssignments.  If the object already has a bunch of SPRoleAssignments in the SPRoleAssignmentCollection, we get rid of them by removing each one.  I do this quickly with a for loop using the SPRoleAssignmentCollection's Count property.

Now that we have a clean SPRoleAssignmentCollection we need to go about populating that collection with the SPRoleAssignments that come from the other list.  Like I mentioned before, permissions are Site Collection based, and, worse yet, SPUser objects are SPWeb based. What does that mean?  If there is no corrisponding SPUser object for your user or group, you can not assign permissions to them.  Even if your list inherits its permissions from the web, and your web is set to allow all Authenticated Users, if the particular user that you want to assign permissions to has not accessed the target web, there is no SPUser object for that user.  As far as the SPWeb is concerned the user doesn't exist.  This can be a real problem.
How do we get around it?  Well, we need to first add the user or group to the web, then create a SPRoleAssingment that will contain that user and map permissions.  That brings us to the next section of code:


foreach (SPRoleAssignment role in sourceRoles) {
                                SPUser newUser;
                                try {
                                    newUser = web.EnsureUser(role.Member.LoginName);
                                    web.EnsureUser(role.Member.LoginName);
                                } catch {
                                    continue;
                                }
                                web.AllowUnsafeUpdates = true;
                                SPRoleAssignment newAssignment = new SPRoleAssignment(newUser.LoginName, newUser.Email, newUser.Name, "");
                                foreach (SPRoleDefinition sourceDef in role.RoleDefinitionBindings) {
                                    string defName = sourceDef.Name;
                                    if (defName == "Limited Access") {
                                        continue;
                                    } else {
                                        try {
                                            SPRoleDefinition newDef = web.RoleDefinitions[defName];
                                            newAssignment.RoleDefinitionBindings.Add(newDef);
                                            targetListItem.RoleAssignments.Add(newAssignment);
                                        } catch {
                                            continue;
                                        }
                                    }
                                }
                            }
                        }
                        web.AllowUnsafeUpdates = false;
                    }
                }
            });
        }


First, I create a loop that will take me through each SPRoleAssingment that the source SPListItem or SPList object has.  Then I create a SPUser.
Now is where things get sticky.  The way I have this code really isn't the best way to solve this, but it worked for me, and I will fix it another time...  Maybe...
What I do first is to fill out the SPUser object by calling the SPWeb.EnsureUser method.  This is a very handy method that will check if the user or group exists in the current context, then add it to the current context if it does not.
If you have any SharePoint groups associated with your permissions you are going to have a hard time.  You will either need to write code to create groups with the same name and permissions in your web, or create these groups ahead of time.
I use Active Directory groups for my permissions, so I don't care at all about SharePoint groups that may be in the SPRoleAssignmentCollection.  I just ignore them, thus the try\catch block that does nothing other than go on to the next permission if there is an exception.  Really, the only exception that can occur is the one that says that the SharePoint group does not exist in the current context.  I don't care about that, because AD groups, as long as they are in AD, can be added directly.

Next we make sure that the Unsafe Updates, like permission changes, are allowed.  One minor gotcha is that if the SPRoleAssingmentCollection.Add method is called, the AllowUnsafeUpdates bool is switched back to false automatically.  We need to confirm that it is True so that we can do our permissions update.

Now we create the new SPRoleAssignment that we will join up this SPUser that we just created with its proper permissions on the SPListItem.  We pass in the properties of the SPUser in to the SPRoleAssignment  object.  Now, it might be tempting to simply pass the SPRoleAssignment.Member object to the new SPRoleAssignment object that we are trying to create.  BUT remember that the SPRoleAssignment.Member is a member of the OTHER site collection.  Not the site collection that houses the target list item.  If we do pass in the SPRoleAssignment.Member from the source site collection, the code will build and it will even execute, BUT the results will NOT be what you are expecting.  In my development envornment I saw the proper object being passed in to the target item's SPRoleAssingmentCollection, but when I checked the collection after the add method was called, I saw that the Member had changed to be the first SPUser that had the same permissions in the web.  Very strange!!
So, to avoid this very real and very dangerous gotcha, we create a brand new SPRoleAssignment and pass in the SPUser properties.

Next I pick apart the actual permission bindings in the form of SPRoleDefinitions.  First, off...  What really annoys me about this particualr section is that Microsoft changed the way they name their collections.  Nearly every collection they have they name so it is very easy to deduce what the colection contains.  The SPSite.Webs is a collection of SPWebs.  The SPList.Items is a collection of SPListItems.  What is the SPRoleAssignment.RoleDefinitionBindings a collection of?  SPRoleDefinitionBindings?  No such object.  It is a collection of SPRoleDefinitions.  Not terribly different from the standard naming convention, but still enough to mess with you and prevent your code from building.
 Anyway, because the same user or group can have multiple permissions assigned to it, you need to create a SPRoleDefinition for each permission and add it to the collection.
Now, a show stopping gotcha appears.  There may be a user, like the System Account or the Search Account, that gets added to the list by the system, and is assigned "Limited Access."  Limited Access is a special permission type, that you can not add a role.  It is a system reserved permission level.  But, it will show up in the RoleDefinitionBindings collection.  It will cause your code to fail if you try to assign this permission level to a user, so, you need to have some code that will handle this possibility.  I simply continue my foreach loop if I encounter it.  You could use some LINQ to filter it out or something else, but, since the foreach loop is pretty performant, I just move on to the next one.

Now we get to the business binding a permission to a user.  We have our user created and added to the web, we have our user added to to the SPRoleAssignment, now we create the SPRoleDefinition to add the permission to the collection.  Because our permissions are going to be named the same, unless you created your own permission levels, then you would need something that would add a similar permission level to your target web, we can just grab the name of the permission from the source web and look it up in the target web.  Like most objects in SharePoint the SPWeb.RoleDefinitions object has an index that you can pass the string name in to.  Here I assign the name of the source definition to a string variable and pass that as the index to the SPWeb.RoleDefinitions object.
Now that I have a RoleDefinition, I add it to the SPRoleAssignment.RoleDefinitionBindings collection.  Then, finally, I add the SPRoleAssignment to the target SPListItem.RoleAssignments collection.
I continue all the way through for all of the objects that are in the source SPListItem.
Cleaning up, I make sure that the target SPWeb has its AllowUnsafeUpdates flag set to false.
Returning to the calling method, I call the SPListItem.Update() method to commit all changes, and finally make sure that the source SPWeb has its AllowUnsafeUpdates flag set to false.

Not horrifically difficult, but there are several gotchas that tripped me up.  I hope you are able to step around them!!!
Here are both of my methods in full form:
private void ConfigurePermissions(SPListItem targetListItem, SPListItem sourceListItem) {
            SPSecurity.RunWithElevatedPrivileges(delegate() {
                using (SPSite sourceSite = new SPSite(sourceListItem.Web.Site.ID)) {
                    using (SPWeb sourceWeb = sourceSite.OpenWeb(sourceListItem.Web.ID)) {
                        sourceWeb.AllowUnsafeUpdates = true;
                        if (sourceListItem.HasUniqueRoleAssignments && sourceListItem.RoleAssignments != targetListItem.RoleAssignments) {
                            SPRoleAssignmentCollection sourceRoles = sourceListItem.RoleAssignments;
                            PropogatePermissions(sourceRoles, targetListItem);
                            targetListItem.Update();
                        } else if (sourceListItem.ParentList.HasUniqueRoleAssignments && sourceListItem.ParentList.RoleAssignments != targetListItem.RoleAssignments) {
                            SPRoleAssignmentCollection sourceRoles = sourceListItem.ParentList.RoleAssignments;
                            PropogatePermissions(sourceRoles, targetListItem);
                            targetListItem.Update();
                        }
                        sourceWeb.AllowUnsafeUpdates = false;
                    }
                }
            });
        }

        private void PropogatePermissions(SPRoleAssignmentCollection sourceRoles, SPListItem targetListItem) {
            SPSecurity.RunWithElevatedPrivileges(delegate() {
                using (SPSite site = new SPSite(targetListItem.Web.Site.ID)) {
                    using (SPWeb web = site.OpenWeb(targetListItem.Web.ID)) {
                        if (sourceRoles != null) {
                            if (targetListItem.HasUniqueRoleAssignments) {
                                for (int i = 0; i < targetListItem.RoleAssignments.Count; i++) {
                                    targetListItem.RoleAssignments.Remove(i);
                                }
                            } else {
                                targetListItem.BreakRoleInheritance(false);
                            }
                            foreach (SPRoleAssignment role in sourceRoles) {
                                SPUser newUser;
                                try {
                                    newUser = web.EnsureUser(role.Member.LoginName);
                                    web.EnsureUser(role.Member.LoginName);
                                } catch {
                                    continue;
                                }
                                web.AllowUnsafeUpdates = true;
                                SPRoleAssignment newAssignment = new SPRoleAssignment(newUser.LoginName, newUser.Email, newUser.Name, "");
                                foreach (SPRoleDefinition sourceDef in role.RoleDefinitionBindings) {
                                    string defName = sourceDef.Name;
                                    if (defName == "Limited Access") {
                                        continue;
                                    } else {
                                        try {
                                            SPRoleDefinition newDef = web.RoleDefinitions[defName];
                                            newAssignment.RoleDefinitionBindings.Add(newDef);
                                            targetListItem.RoleAssignments.Add(newAssignment);
                                        } catch {
                                            continue;
                                        }
                                    }
                                }
                            }
                        }
                        web.AllowUnsafeUpdates = false;
                    }
                }
            });
        }

Thursday, September 20, 2012

Recruiters

Recruiters...  You guys can really get under my skin.  I get that you have a job to do, and that job entails getting me to leave mine.  I know that you only get paid if I leave my job, and your bonus depends on how much in salary I get paid.  It is a tough job, because the work is on both ends, you need to sell the job to me, and then sell me to the employer.  There is a lot that can go wrong, and I understand that it can be frustrating for you.
HOWEVER, if you are a professional IT recruiter, you need to do a little bit of research in to the actual IT world to know what the hell you are talking about.  There are several things you do that drive me up a wall, and a few that will make me want to never ever use you to nail down an opportunity, no matter how lucrative.
Since this is my blog, and I am a nice guy, Imma give you all some great advice instead of a listing of my pet peeves about recruiters.  In all seriousness, this will help you do better at your job, and will help you gain the trust of your candidates.


  • Know SOMETHING about the skills you are recruiting for.
    • This has got to be my absolute biggest pet peeve about recruiters.  When they call/email me asking about my skill set, and have absolutely no clue about what they are asking about.  I loose all confidence in you representing me when you first ask me if I know .NET, then ask me if I know Visual Studio.  Visual Studio is the programming platform for .NET.  Yes, I know you could use something else to write code in, but NOBODY does.  Don't ask me if I have worked in the Windows Communication Framework, then ask me if I know WCF.  They are the same thing.  Don't listen to my expereince in designing service frameworks using WCF, and ask me if I have ever done SOA development.  Don't ask me if I have developed web parts for SharePoint, then ask me if I know ASP.NET.  SharePoint IS ASP.NET.  ARRRGGGHHH!!!!
    • With just a little bit of reading, and I mean just a little bit, less than 1/2 hour, you could look up those terms on the internet and know, at a very high level, EXACTLY what they are about.  You are a damn professional, do your research!!!!
    • I will forgive a recruiter a lot.  You are not truly IT people, so I don't expect you to know what the MVVM patter is, or where to go when implementing a Managed Metadata Taxonomy.  I do expect you to know the very very very basics so you don't ask any redundant or misguided questions.  If you don't know what my skills are, how can I trust you to find a position that I will fit in to?  If I am hiring you to find me someone, how can I trust you to find me the right person?
  • Never cold call someone to ask them what a technology is.
    • I must get one of these every month.  "Hey, this is Bob from Bob's Tech Recruiting Company.  I saw your resume on line and I wondered if you could tell me what 'SharePoint' is."
      This gives me NO confidence in you representing me.  I will politely direct you to Microsoft's web site for your information after a very short description of what SharePoint is.
      If you have a need for a SharePoint professional, simply come out and say it.  If you don't know what it is, GOOGLE!  Do your research.
  • Look at the candidate's resume first to determine experience.  Make an educated guess as to weather the job that you are looking to fill will fit in to the candidate's experience level.
    • I have written my resume to be very easy to go through.  You can easily see with just a glance that I have many years experience in the stated technologies.  You inspire very little confidence as someone who takes their job seriously if you submit to me entry level jobs when my experience is clearly senior level.  The same is true for an entry level guy getting submitted for senior level positions.
  • Do not balk when you hear salary requirements. 
    • Yes, I might make more than you do.  I also have more experience than you do.  Don't make snide comments, cough, say wow, or make any other comments.  I might be tougher to place than you think.
      I realize the difficulty of finding a job that will keep me in the same pay scale that I am accustomed.  I have been in that position before.     
  • Never simply drop a correspondence if the employer isn't interested in the candidate.  
    • Let me know that the client is no longer interested in me and the reason why.  It is a courtesy call.  It let's me know that we are in this together and that I can trust you to let me know the hard news.  Knowing the reason why helps me become a better candidate in the future, because I will know what I need to do to sharpen my resume, interview skills, or my personal attitude.
      Remember, the better candidate I am, the more likely YOU are able to place me.
  • This is a business deal.  Do not take rejection personally.
    • You are a professional.  You are working on sealing a business transaction.  If I don't like the job or company that you are submitting to me, how is that a personal attack on you?  Don't get upset if I am not ready to leave my current position, or I just don't like what you have put in front of me.  
  • Money is not the only thing that will motivate a candidate to switch jobs.   
    • I have had HUGE numbers thrown at me to work for companies.  I have turned them down.  The reason is that I didn't like the company culture, or some other facet of the job smelled wrong to me.
      I really hate it when recruiters ask over and over again, how much is it going to take to get you to move?  Or this employer is willing to pay more for you.  Blah blah blah.
      Find out what the candidate needs to move.  For me, I will take a significat hit on my salary if more vacation time is offered.  I will sacrifice salary AND vacation time if the opportunity will gain me experience in something that I really want experience in.
      I moved from my last job because my current job offered global deployment experience.  My last job didn't pay me more, but gave me a bunch of vacation time and full benefits.  I don't get benefits now.
      Find out what is important to that candidate and see if the employer will move their direction.

Monday, September 3, 2012

ItemDeleting Event Not Firing

I have written many event receivers.  I have written them for just about every conceivable business use, and for some that are not so conceivable.
I am a MCPD, passing both tests in the 90% range.  I have written these receivers for SPS 2003, MOSS 2007, SharePoint 2010, and even on SharePoint 2013 Beta.

I'm kind of a big deal.

So, why, when I was writing such a mundane application as an event receiver, did one of the events not fire? I overrode the proper method.  I used the correct syntax.  I had no runtime exceptions, and everything built and deployed just fine.  During debugging, as this was a farm deployed solution, I attached to the correct w3wp.exe worker process application (if it would have been a sandboxed solution I would have attached to the user worker process application, SPUCWorkerProcess.exe).  I knew this, because the other events I overrode were firing and doing their jobs just fine.  BUT when it came to the delete events nothing...  What the heck?????????

After looking around and pulling out more than a little bit of hair, out of frustration I created a new solution with just the delete events.  This solution worked just fine.  No muss no fuss.  So, why would the very same code not work in one solution, but not in another???   I did a file comparison of the wsp files to find out what was up.

What happened was this...  When I initially created the project in Visual Studio, I checked the box for just the ItemUpdating event.  Nothing else.  What Visual Studio does with this setting is it adds in the Elements.xml a <Receiver> section in a <Receivers> block.  In that block is an element called <Type>.
What I had forgotten to do with my first solution was to add a <Type> element for my ItemDeleting event.  The Elements.xml file registers the receiver with SharePoint.  If it doesn't have a <Type> section for your events, it doesn't matter how much code you have written in as many overridden methods, the will not fire.
Now, the ItemDeleting event is a separate receiver event, thus it does need its own <Receiver> section.
I just copied my ItemUpdating receiver, and pasted it below.  Then I changed the name and the <Type>.  Everything else can remain the same.

After realizing my mistake, I added the proper <Type> elements to register my deleting events, and BLAMO, they fired just as they should have.
One little trick that may catch many people is that you may have to deactivate then re-activate your feature in order to register the new receiver section, and get the event to fire.  For whatever reason, SharePoint may not load the new XML, even after the IIS Application Pool is recycled.  It can be very annoying, ESPECIALLY if you are trouble shooting other reasons why your event is not firing.

It was a rookie mistake that I made.  Nearly every part of SharePoint development has two parts to it.  The code or assembly part, and the declarative XML part.  The code never works without the XML letting SharePoint know what's up.

One of the good things about software development is that the code will always find away to keep you humble.

Wednesday, August 22, 2012

Managed Metadata, Lists, and the TaxonomyField

My client has several document libraries in many different Site Collections.  All of the site collections use a global Managed Metadata Service.

My client very much likes how lists can be filtered by the Managed Metadata Navigation on SharePoint 2010 Lists.  So, with all of those document libraries scattered all over the place, and the ease with which we can use Managed Metadata Navigation, why not create a list in the root site of the portal site collection that has links to ALL of the documents in ALL of the Site Collections???  You can do that right????  Sure, I say, sounds like fun.  Woof.

I am not a fan of duplicating data, especially when that data is potentially very large documents.  However my client simply wanted a link back to the original document and the ability to filter the documents just on the Global Managed Metadata...  So, what to do?  Essentially we create a list that has two fields, besides the title, a hyperlink field and a Managed Metadata field. Great!!  That's easy.  Now we need to populate it...  Boooo!!!  That's not so easy.
Well...  I will qualify that.  It wasn't easy until I finally got my head around just what the Taxonomy Fields were, how the Managed Metadata service worked, and all of the gotchas that surround it.

My solution was to create an Event Receiver that would look at document libraries, and if the list's ID matched one in a configuration list, an item would be created in the Master List that had a link to the document on the source list and a managed metadata field that corresponded with the term in the source list. Getting the URL to the document was easy. The tough part came with the Manged Metadata field.

First, because this was a big part of my problems, you have to get your head around the fact that the Managed Metadata fields are VERY different from any other SPField object you have ever worked with.  It is essential to look at them as completely separate entities from the normal SPFields out there.  Your first clue to this is that you can't just start declaring things as TaxonomyFields, that is what the SPField.Type is that contains the Managed Metadata stuff...  For one, TaxonomyFields do not reside in the normal Microsoft.SharePoint namespace, like the SPField objects do.  You need to create a reference to the Microsoft.SharePoint.Taxonomy.dll.  That is where those guys live.

Next, when you are typically using the object model to set a list item equal to another list item, it is pretty elementary.  You have your SPListItem, you use the handy dandy index to set your field, you do it for the other item as well and you set them equal to one another.  Easy peasy.
SPListItem.Items["Goof"] = SPListItem.Items["Ball"]

Sometimes the list items get a little bit more complex, and you have to use a SPFieldValue object to populate them.  Like a SPFieldUrl field.  You set it using the SPFieldUrlValue object.
SPFieldUrlValue.Url = "http://ramblingsofamadcomputerguy.blogspot.com/"
SPFieldUrlValue.Description = "Some Jerk's Blog"
SPListItem.Items["HyperLink"] = SPFieldUrlValue

In both cases you are simply setting the value with some strings that are associated with that SPField's type.  This is NOT the case when you are dealing with Managed Metadata.   Microsoft wanted to make things extra specially vague by adding things to site collections such as hidden text fields hidden lists.  And then requiring that you know these hidden things before you can set values of anything.  Way to go Microsoft.  Imma yell at you at the next SharePoint Conference ...  Not that you will care, but it will make me feel a whole lot better.

So we come to our first and most serious gotcha.  Metadata terms, like nearly all of SharePoint's things, all have a GUID.  In Managed Metadata terms this is called the SSPID.  These are easy to find, and actually can be brought up from your TaxonomyField object.  You would think that this, Global Unique IDentity , along with a friendly name, would be all that you need.  After all the ID is GLOBAL!!!!  SharePoint could just look it up in the store.  Right??  Right?????   Nope.  You also need something called a WSSID.
A what now??  Just as it implies the WSSID is an ID unique to the site collection with with the term is being used.  See where the gotcha is coming?
What if the term has never been used in the Site Collection?  How are we supposed to know what the WSSID is if it does not yet exist???  Bingo.  The million dollar question.

So this is what you need to set your TaxonomyField correctly.  A term name, a term GUID, and a WSSID.  Great.  All is not completely lost, our friends at Microsoft have given us away out of the woods.

So, there are a few things that we need to do.  First, we need a TaxonomyField set to the TaxonomyField in our list.  Notice something very critical here.  It is called a TaxonomyFIELD.  Because it IS a specific type of SPField object.  So when you are instantiated your TaxonomyField from a list you must use the FIELD object in the list. Lets look at some code. First we instantiated the TaxonomyFields from the list


SPField list1Field = item.Fields["Source"];
 SPListItem itemToAdd = list2.Items.Add();
 itemToAdd["Title"] = "Source";
 TaxonomyField list1TaxField = (TaxonomyField)item.Fields["Source"];
 TaxonomyField list2TaxField = (TaxonomyField)itemToAdd.Fields["Target"];


In the code you can see that the TaxonomyFields are instantiated by using the Fields collection of the SPListItem, NOT from setting it equal to the the VALUE of the item. This is important. You are instantiated a FIELD. We will set the value of this field later on.

Next we need to create the very thing that will populate the field we just instantiated. It is called a TaxonomyFieldValue object. Remember that C# is a strongly typed language, so if you are setting the TaxonomyFieldValue object equal to a value that you already have, you do need to cast objects correctly.
After you have the TaxonomyFieldValue object, we need to create a new one for the list item in our target list. This is where a second gotcha comes in. You can't just cast the source value as TaxonomyFieldValue and set it equal to the new TaxonomyFieldValue. Why? Because the values are NOT the same. For one, if you are crossing Site Collection boundaries, you won't have the same WSSID. Thought I wouldn't get to that didn't you??? There are some other things wrong, but rest assured setting them equal isn't the way to go.

Now I have been focusing a lot on what if the term hasn't been used before. What if it has been used, or if you are coping to another list in the same site collection. How would I get the WSSID? I'll go over this quickly, because you really don't want to be messing too much with where you get the WSSID, because there are much easier ways to go about it.

First the WSSID is a numerical field that represents the term in a very secret list. It is called the TaxonomyHiddenList. Sounds spooky right? You can look up the term in that list, just as you would any other SharePoint list. You take the ID of the SPListItem that holds the term and you have the WSSID. NEVER MESS WITH THE TaxonomyHiddenList!!!!!!!!!!! It will seriously ruin you day. Just stay away from it. It is one of those things behind the scenes that makes SharePoint work. Just don't go looking for it. It is bad news. Instead, use the API that comes with the Microsoft.SharePoint.Taxonomy namespace.

You can use the TaxonomyField.GetWssIdsOfTerm method to look up the WSSID in the TaxonomyHiddenList, without actually writing code to directly access the TaxonomyHiddenList. But if you use that method, you will need to write logic to do something else if the term does not exist. There is an easier way. It works in all situations and is elementary to use. It is the TaxonomyFieldValue.PopulateFromLabelGuidPair method.

The TaxonomyFieldValue.PopulateFromLabelGuidPair method will add the the term to the TaxonomyHiddenList if it does not exist, or it will automatically populate your TaxonomyFieldValue object with the proper WSSID if the term does exist. It is a very handy tool. It does everything you want, nothing you don't. Use this guy!!!
Now, you could set the WSSID to -1 and that will work similarly to the TaxonomyFieldValue.PopulateFromLabelGuidPair method, but... I don't like that as much. Microsoft is reluctant to change the way their methods work for backwards compatiability reasons, but seem to have no problem messing with how it uses "reserved" values such as -1. I think that the TaxonomyFieldValue.PopulateFromLabelGuidPair method is the best way to go.

Ok, all of that said, what does the TaxonomyFieldValue.PopulateFromLabelGuidPair use? It uses a delimited string of the friendly term name and the term GUID. The dilimter is the pipe "|".

I typically use the TaxonomyFieldValue.Label and the TaxonomyFieldValue.TermGuid properties put together with string.Format. It makes it very easy.

After that we have everything we need to set the value of our TaxonomyField. We use the TaxonomyField.SetFieldValue method and pass in the SPListItem that contains our TaxonomyFiled along with the TaxonomyFieldValue. Update the SPListItem and we are done!!!

//Set the TaxonomyFieldValue with the value of the first list item
TaxonomyFieldValue list1TaxFieldValue = (TaxonomyFieldValue)item["Source"];

//Create a new TaxonomyFieldValue with the TaxonomyField in the target list
TaxonomyFieldValue value = new TaxonomyFieldValue(list2TaxField);

/*Create the WSSID using the values for the term name and its GUID.  Notice that the values are seperated by the pipe.*/
value.PopulateFromLabelGuidPair(string.Format("{0}|{1}", list1TaxFieldValue.Label, list1TaxFieldValue.TermGuid));

//Use the SetFieldValue method with the SPListItem and the TaxonomyFieldValue
list2TaxField.SetFieldValue(itemToAdd, value);

//Update the SPListItem
itemToAdd.Update();



Not very straight forward, but not excessively difficult either. I don't know if this is changed in SharePoint 2013, but I would expect that it isn't. Microsoft, reportedly, makes much greater use of Managed Metadata in SharePoint 2013, so it would be a good idea to familiarize yourself with it.

Thanks to Nick Hobbs. His blog post showed me the way!!