As part of our daily jobs, we should rename projects on our Project Server occasionally. For this kind of change, we have already a “human workflow” or a check list: tasks, we should perform one after another.
The Standard Process
These steps include:
1. Changing the project name on the Project Details page:
2. Setting the project web site (PWS) title and URL on the Title, Description, and Logo page of Site Settings:
(Note, there is a bug already on this page. If the URL is long enough, it is displayed duplicated in the path under URL name, once in the fix part, and once in the text box. It is already part of the example URL bottom on the left as well. See the screenshot above.)
3. Re-binding the project to the relocated PWS via the PWA Settings / Connected SharePoint Sites / Edit Address.
(For more information, see The Edit Site Address settings on this page)
4. Beyond the steps described above we have some extra steps, like renaming project groups, setting further PWS properties, and so on, but these steps are all custom to our current solution.
The Problem
A few month ago one of the Project Server administrators complained that he is not able to change the site address of PWS. He was to change the URL from (let’s say) VeryVeryLongProjectSiteUrl to VeryVeryLongProjectSiteUrlNew. Although he has not got any error message, and I’ve not found any related entry in the ULS logs either, the original URL of the PWS remained unchanged.
Changing the Site Address via PSI and PowerShell
First, I wrote a PowerShell script that uses the PSI to change the PWS binding via the UpdateProjectWorkspaceAddress method.
- $pwsCurrentUrl = "http://YourProjectServer/PWA/VeryVeryLongProjectSiteUrl"
- $projUrl = "PWA/VeryVeryLongProjectSiteUrlNew" # that is the destination URL of the PWS, it should be the server relative URL, including PWA in the path!
- $web = Get-SPWeb $pwsCurrentUrl
- # if you already know the IDs (project ID and site ID of the PWA site)
- # $projId = [Guid]"99894c16-7a03-e411-83c6-005056b45654"
- # $siteId = [Guid]"e1b9fba5-09ad-441a-8679-6286dde059ab"
- # or get the IDs from the PWS properties
- $projId = $web.AllProperties["MSPWAPROJUID"]
- $siteId = $web.Site.Id
- # figure out the PWA url dinamically
- # $pwaUrl = $web.AllProperties["PWAURL"] # or
- $pwaUrl = $web.Site.Url
- # we are using the Project PSI service
- $svcPath = "/_vti_bin/psi/Project.asmx?wsdl"
- # https://social.technet.microsoft.com/Forums/scriptcenter/en-US/9d0d73bb-b2bf-4528-beea-321cf82a9b89/problem-executing-a-script-what-uses-namespace-parameter-in-the-newwebserviceproxy-cmdlet
- If ($global:svcPSProxy -eq $null)
- {
- Write-Host "Connecting PSI proxy at $pwaUrl …"
- $global:svcPSProxy = New-WebServiceProxy -Namespace PSIProxy -Uri ($pwaUrl + $svcPath) -UseDefaultCredential
- }
- Else
- {
- Write-Host "Reusing existing PSI proxy"
- }
- # change the project – PWS binding, or create a new PWS if there is no PWS at the destination
- $svcPSProxy.UpdateProjectWorkspaceAddress($projId, $projUrl, $siteId)
Note, that based on my tests, the script not only maps an existing PWS to the project, but it creates a new PWS if there is no PWS at the destination URL specified.
Finding the Bug on the Web Page
After completing the task via the script above, I decided to find out the reason, the UI does not work in this case.
As far as I see, it is a simple silly error in the JavaScript on the Edit Site Address page (\TEMPLATE\LAYOUTS\PWA\ADMIN\EditSiteAddressDlg.aspx).
It is the Init function on that page:
- function Init()
- {ULSH9J:;
- oArgs = window.frameElement.dialogArgs;
- sProjName = oArgs.sProjName;
- sServerAddr = ((oArgs.sServerAddr != null) ? oArgs.sServerAddr : "");
- sSubwebName = oArgs.sSubwebName;
- idProjectNameTD.title = sProjName;
- if(sProjName.length > 40)
- {
- sProjName = sProjName.slice(0,40) + "…";
- }
- XUI.Html.SetText(idProjectNameTD, sProjName);
- if((sServerAddr != "") && (sSubwebName != ""))
- {
- var sUrl = sServerAddr + "/" + sSubwebName;
- idServerAddressTD.title = sUrl;
- if(sUrl.length > 40)
- {
- sUrl = sUrl.slice(0,40) + "…";
- }
- XUI.Html.SetText(idServerAddressTD, sUrl);
- }
- idSubwebName.value = sSubwebName;
- RecalculateTargetURL();
- origTargetUrl = XUI.Html.GetText(idTargetURL);
- }
This function invokes the RecalculateTargetURL function (see below) to trim the end of the URL of the PWS if it is longer then 50 characters, and to append … to it. This value is displayed then on the page as Destination URL. In the Init function we store the original value in the origTargetUrl variable.
- function RecalculateTargetURL()
- {ULSH9J:;
- var sURL = idVirtualServerDropdown[idVirtualServerDropdown.selectedIndex].text;
- sURL += "/" + TrimSpaces(idSubwebName.value);
- idTargetURL.title = sURL;
- if(sURL.length > 50)
- {
- sURL = sURL.slice(0,50) + "…";
- }
- XUI.Html.SetText(idTargetURL, sURL);
- }
The very same RecalculateTargetURL function is invoked on each key press or on changes in the Site URL text box to keep the value of the Destination URL on the page current:
<input DIR="ltr" type="text" id="idSubwebName" name="idSubwebName" style="width: 160px" onchange="RecalculateTargetURL()" onkeyup="RecalculateTargetURL()" …
Note, that the RecalculateTargetURL function is registered for the onchange event of Web Application dropdown either.
The problem is, that the script uses the trimmed values for comparison in the OkBtn_OnClick function (see the method below, including some server side code) to decide, if there is any change in the URL (see the condition with the comment “If nothing changed then we don’t have to do anything” below). Of course, if you have long site (and project) names, and you change something only at the end of the name, this comparison won’t detect the change.
- function OkBtn_OnClick()
- {ULSH9J:;
- if(idSiteEnabled.checked && (TrimSpaces(idSubwebName.value) == ""))
- {
- XUI.Html.SetText(idAlertBox, PJUnescape("<%=PJEscape(PJUtility.GetLocalizedString(IDS.ADMIN_EDITSITEADDRESSDLG_WEB_NAME_BLANK_ALERT))%>"));
- XUI.Html.SetText(idRequiredFieldIndicator, "*");
- idSubwebName.focus();
- return;
- }
- // If nothing changed then we don't have to do anything.
- if((origTargetUrl == XUI.Html.GetText(idTargetURL)) && !idSiteNotEnabled.checked)
- {
- window.frameElement.commonModalDialogClose(0, null);
- return;
- }
- //if we remove the site
- else if(idSiteNotEnabled.checked)
- {
- idSubwebName.value = "";
- oArgs.sNewSubwebName = "";
- oArgs.sNewServerUID = "<%=Guid.Empty%>";
- }
- //we change the site
- else
- {
- oArgs.sNewServerUID = idVirtualServerDropdown[idVirtualServerDropdown.selectedIndex].value;
- var sTemp = TrimSpaces(idSubwebName.value);
- // Remove the trailing slash.
- if(sTemp.charAt(sTemp.length – 1) == '/')
- {
- sTemp = sTemp.substr(0, sTemp.length – 1);
- }
- oArgs.sNewSubwebName = sTemp;
- window.returnValue = true;
- }
- window.frameElement.commonModalDialogClose(1, oArgs);
- }
Note however, that if you click on the Test URL button, a new browser tab would be opened with the right destination URL (and not the trimmed one). The right new URL is displayed as a tooltip as well, when you move the mouse pointer over the URL right to the Destination URL title.
function TestUrl_OnClick(event)
{ULSH9J:;
window.open(idTargetURL.title);
}
As you can see, the TestUrl_OnClick function uses the tooltip of the Destination URL (idTargetURL.title) to open the site. It is important to point out, that the value of idTargetURL.title is set to the full URL, and not to the trimmed one in the RecalculateTargetURL function (see above).
A Quick Workaround via the Web Page
If you don’t want (or not allowed) to use the PowerShell script above to relocate your PWS, there is a simple workaround that uses the standard web admin UI. Start the F12 Developer Tools in Internet Explorer, and set a breakpoint on the line
if((origTargetUrl == XUI.Html.GetText(idTargetURL)) && !idSiteNotEnabled.checked)
on the Edit Site Address page. If the breakpoint get hit, jump over the condition by setting the next statement of execution direct onto the line:
oArgs.sNewServerUID = idVirtualServerDropdown[idVirtualServerDropdown.selectedIndex].value;
The Long-Term (but Dirty) Solution
Although it is not supported, you can change the code in the EditSiteAddressDlg.aspx page as well. I strongly suggest you to take a backup of this file first.
There are two options to fix the error, the first one is to modify the Init function to save the original full (!) URL instead of the trimmed one:
//origTargetUrl = XUI.Html.GetText(idTargetURL);
origTargetUrl = idTargetURL.title;
Then use this value to compare with the current untrimmed URL value in the OkBtn_OnClick function:
// If nothing changed then we don’t have to do anything.
//if((origTargetUrl == XUI.Html.GetText(idTargetURL)) && !idSiteNotEnabled.checked)
if((origTargetUrl == idTargetURL.title) && !idSiteNotEnabled.checked)
The other option is to forget origTargetUrl, and take the original full URL from the tooltip of the Current site address. As you can see on the screenshot after the RecalculateTargetURL function code snippet above, this tooltip contains the untrimmed URL version.
In this case, the new comparison in the OkBtn_OnClick function:
if((idServerAddressTD.title == idTargetURL.title) && !idSiteNotEnabled.checked)
