By: Dave Scheele
Posted:
September 21, 2008 at 7:31 AM
I am presently developing a large, complex SharePoint application whose deployment consists of about 10 SharePoint Solutions. Since functionality is still under development, I am very frequently rebuilding WSPs and upgrading them.
My standard way of doing this was via the command shell:
stsadm -o upgradesolution -name [solution name] -filename [solution path] -immediate -allowgacdeployment
Less frequently I was adding Solutions to the Solution store (using stsadm -o addsolution) and deploying those Solutions (using stsadm -o deploysolution).
Eventually I got sick of typing these commands, especially upgradesolution, which I used all the time, and I remembered that you can very easily add context menus to Windows Explorer by editing the registry. I now have my development machine set up so I can right-click any WSP in the file system and either Add to Solution Store or Upgrade Solution (Figure 1). This small change has made deployment of my SharePoint Solutions so much easier and I am more productive as a result.
Figure 1
In this article, I will explain how to set this up, so you too can stop typing commands to perform these operations.
The steps required are as follows:
- Edit the registry to specify the context commands you want for the .wsp file extension.
- Set up a batch file for each command which runs stsadm.
To add the context commands to the registry, take the following steps:
- Open regedit.exe and add a key HKCR\.wsp
- Add a key subordinate to HKCR\.wsp and call it shell
- Add a key subordinate to HKCR\.wsp\shell and call it whatever you'd like displayed as the context menu command
- Add a key subordinate to HKCR\.wsp\shell\[commandName] and call it command
- For the Default REG_Z value, assign the command you'd like to execute when the user clicks this context menu item. My pattern is to make a batch file per command and pass the full file path as a parameter.
My Upgrade Solution command, for example, is
"C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\BIN\UpgradeSolution.bat" "%1"
The quotes are important because they allow the shell to gracefully deal with file paths containing spaces. Please see Figure 2 for a picture of how this should look in RegEdit.
Figure 2
Now that you have your commands set up, you need to create your batch files. I chose to put mine in the 12 hive Bin folder. Yes, I know you're thinking I shouldn't put it there, since that is a SharePoint system folder, but it was convenient at the time and of course you can put them anywhere, so choose any location you like - just be sure your command path above is pointing to the right location.
All the batch files will consist of is an stsadm command that extracts pieces of the file path to pass as parameters to stsadm.
For the Add to Solution Store command, the batch file looks like this:
stsadm -o addsolution -filename %1 pause
Notice I am passing the full file path to stsadm as the filename parameter to AddSolution.
For the Upgrade Solution command, the batch file looks like this:
stsadm -o upgradesolution -name "%~nx1" -filename %1 -immediate -allowgacdeployment pause
Just as with AddSolution, UpgradeSolution requires the full file path as the filename parameter. More interestingly, the name of the Solution is always just the WSP filename with extension, and the syntax %~nx1 extracts just the filename.extension portion of a full file path.
Also, notice the name parameter needs quotes surrounding it (in cases of spaces in the name) but the filename parameter does not, because those are already present in what is passed from Explorer.
You might wonder why I need a batch file at all. What can't I just run this command by directly specifying stsadm as the value of the Default REG_SZ value in RegEdit? This does in fact work for AddSolution, where all we need is the full file path, but for UpgradeSolution it didn't work because the %~nx1 doesn't work in that context - only in the context of batch file processing. So although I don't need a batch file for AddSolution, I made one anyway to set up a pattern.
By the way, there are many different operations you can perform on batch file parameters. Windows IT Pro has an informative article describing them.
There are potential enhancements to this, of course. There are commands such as DeploySolution that require other parameters (such as url) that we can't calculate based on file path information. The associated batch file could prompt for this information, or hard code the values, in which case you'd need different commands for different content URLs. Also, you see hard coded paths to the 12 hive in my RegEdit setup, and I would prefer to use something like this:
%CommonProgramFiles%\Microsoft Shared\web server extensions\12\BIN\stsadm.exe
but I was getting permission exceptions when trying to run the command, so this is something to be cleaned up. And finally, I should probably put the batch files elsewhere in the file system.
Give this a shot if you work frequently with SharePoint Solutions. The 5 minutes of setup time will pay itself back very quickly as a result of your enhanced productivity.
I have supplied the RegEdit export below. Just make a .REG file out of this and run it to add the values to your registry. Of course, the usual disclaimers about registry operations apply - be careful, don't do this if you don't know what you are doing, and make a backup of your registry first in case you mess it up.
Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT\.wsp] @="wspfile"
[HKEY_CLASSES_ROOT\.wsp\shell]
[HKEY_CLASSES_ROOT\.wsp\shell\Add to Solution Store]
[HKEY_CLASSES_ROOT\.wsp\shell\Add to Solution Store\command] @="\"C:\\Program Files\\Common Files\\Microsoft Shared\\Web Server Extensions\\12\\BIN\\AddSolution\" \"%1\""
[HKEY_CLASSES_ROOT\.wsp\shell\Upgrade Solution]
[HKEY_CLASSES_ROOT\.wsp\shell\Upgrade Solution\command] @="\"C:\\Program Files\\Common Files\\Microsoft Shared\\Web Server Extensions\\12\\BIN\\UpgradeSolution.bat\" \"%1\""
By: Dave Scheele
Posted:
September 30, 2006 at 8:03 AM
If your application needs to move a file, one way to do this is to use the .NET Framework System.IO.File.Move method, which moves the specified file from one file system location to another. Unfortunately, this method does not permit you to cancel the operation, nor does it provide progress information. So using this method to copy a 4GB VHD, for example, would appear to freeze your application.
If you would like to provide move file functionality with the ability to cancel the operation and get progress information, you can use the Windows API MoveFileWithProgress. This function lives in Kernel32.dll and requires you use .NET Framework P/Invoke to access it. The basic idea is that you call the API specifying the source and target paths as well as a pointer to a callback function. The callback function is called by Windows every time a chunk of the file is copied, with information about the total file size, total bytes copied so far, and a cancel option.
I won't include the full text of the class inline, but a C# source file is attached which wraps the MoveFileWithProgress Windows API within a .NET class. The class exposes a Progress event using the standard .NET Framework event pattern, so the clients of the class don't have to worry about the Windows-specific stuff. The client simply needs to instantiate the class, register a delegate pointing to the desired progress callback function, and call the Move method.
The key part of the implementation is the MoveFileWithProgress declaration:
/// <summary> /// The MoveFileWithProgress function moves a file or directory. MoveFileWithProgress is equivalent to the MoveFileEx function, /// except that MoveFileWithProgress allows you to provide a callback function that receives progress notifications. /// </summary> /// <param name="lpExistingFileName">The existing file or directory on the local computer.</param> /// <param name="lpNewFileName">The new name of the file or directory on the local computer. When moving a file, lpNewFileName can /// be on a different file system or volume. If lpNewFileName is on another drive, you must set the MOVEFILE_COPY_ALLOWED flag in dwFlags. /// When moving a directory, lpExistingFileName and lpNewFileName must be on the same drive. </param> /// <param name="lpProgressRoutine">Pointer to a CopyProgressRoutine callback function that is called each time another portion of the file has been moved. /// The callback function can be useful if you provide a user interface that displays the progress of the operation. This parameter can be Null.</param> /// <param name="lpData">Argument to be passed to the CopyProgressRoutine callback function. This parameter can be Null.</param> /// <param name="dwFlags">Move options.</param> /// <returns></returns> [DllImport("kernel32.dll", SetLastError=true)] private static extern bool MoveFileWithProgress( string lpExistingFileName, string lpNewFileName, CopyProgressRoutine lpProgressRoutine, IntPtr lpData, uint dwFlags );
The progress callback function has the following signature and examines the callback reason and takes action on the CALLBACK_CHUNK_FINISHED value:
/// <summary> /// The CopyProgressRoutine delegate is an application-defined callback function used with the CopyFileEx and MoveFileWithProgress functions. /// It is called when a portion of a copy or move operation is completed. /// </summary> /// <param name="TotalFileSize">Total size of the file, in bytes.</param> /// <param name="TotalBytesTransferred">Total number of bytes transferred from the source file to the destination file since the copy operation began.</param> /// <param name="StreamSize">Total size of the current file stream, in bytes.</param> /// <param name="StreamBytesTransferred">Total number of bytes in the current stream that have been transferred from the source file to the destination file since the copy operation began. </param> /// <param name="dwStreamNumber">Handle to the current stream. The first time CopyProgressRoutine is called, the stream number is 1.</param> /// <param name="dwCallbackReason">Reason that CopyProgressRoutine was called.</param> /// <param name="hSourceFile">Handle to the source file.</param> /// <param name="hDestinationFile">Handle to the destination file.</param> /// <param name="lpData">Argument passed to CopyProgressRoutine by the CopyFileEx or MoveFileWithProgress function.</param> /// <returns>A value indicating how to proceed with the copy operation.</returns> protected uint CopyProgressCallback( long TotalFileSize, long TotalBytesTransferred, long StreamSize, long StreamBytesTransferred, uint dwStreamNumber, uint dwCallbackReason, IntPtr hSourceFile, IntPtr hDestinationFile, IntPtr lpData ) { switch ( dwCallbackReason ) { case CALLBACK_CHUNK_FINISHED: // Another part of the file was copied. CopyProgressEventArgs e = new CopyProgressEventArgs( TotalFileSize, TotalBytesTransferred ); InvokeCopyProgress( e ); return e.Cancel ? PROGRESS_CANCEL : PROGRESS_CONTINUE;
case CALLBACK_STREAM_SWITCH: // A new stream was created. We don't care about this one - just continue the move operation. return PROGRESS_CONTINUE;
default: return PROGRESS_CONTINUE; } }
The public Move method simply delegates to the Windows API function and throws a .NET exception if the API returns a failure:
/// <summary> /// Move a file or directory from source to destination with copy progress reports. /// </summary> /// <param name="sourceFile">The file or directory to be moved.</param> /// <param name="destinationFile">The new name of the file or directory.</param> public void Move( string sourceFile, string destinationFile ) { bool success = MoveFileWithProgress( sourceFile, destinationFile, new CopyProgressRoutine( this.CopyProgressCallback ), IntPtr.Zero, MOVEFILE_COPY_ALLOWED | MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH ); // Throw an exception if the Move failed. if ( ! success ) { int error = Marshal.GetLastWin32Error( ); throw new Win32Exception( error ); } }
As I mentioned above, the attached class has a full implementation including definition of all the symbols required.
By: Dave Scheele
Posted:
September 30, 2006 at 8:03 AM
The .NET Framework HttpWebRequest permits the developer to access resources on a server using the HTTP or HTTPS protocols. Some very secure systems, however, require a client X509 certificate as evidence to access resources. Setting this up in an ASP.NET application is not straightforward because the default ASP.NET service account has limited permissions and does not have the required access to the local certificate store.
The basic idea is to add the desired X509Certificate to the ClientCertificates collection on the HttpWebRequest object before calling the GetResponse method. Creating the X509Certificate object requires the private key of the certificate first be exported to a file in the file system which acts as the input to the X509Certificate::CreateFromCertFile static method. However, in order to successfully access the certificate in the certificate store, it is necessary to execute the code under the account used to install the certificate. We accomplish this by running the code as a COM+ serviced component, configuring the component to run using the credentials of the account under which the certificate was installed.
The example I give here is doing an HTTP POST to the server, attaching the X509 certificate before open the request stream, writing the POST data, and requesting the response.
First, here is the method used to attach the certificate to the HttpWebRequest:
/// <summary> /// Attach an X509 client certificate to an existing Http request. /// </summary> /// <param name="request">The Http request to which to attach the client certificate.</param> /// <param name="userName">The user account name (machinename\username) under which the client certificate was installed.</param> /// <param name="certPath">The path to the exported client certificate file.</param> protected void AttachClientCertificate( HttpWebRequest request, string userName, string certPath ) { TraceWriteLine( "Attaching X509 certificate to HttpWebRequest" );
X509Certificate certificate = ( X509Certificate) _certificates[ certPath ]; if ( certificate == null ) { TraceWriteLine( "X509 certificate not in cache: Creating from file and caching" ); certificate = X509Certificate.CreateFromCertFile( certPath ); _certificates.Add( certPath, certificate ); } request.ClientCertificates.Add( certificate );
TraceWriteLine( "X509 certificate successfully attached to HttpWebRequest" );
}
And here is the method used to do the HTTP POST, which makes use of the AttachClientCertificate method:
/// <summary> /// Posts data to specified Uri using HTTP POST, optionally attaching an X509 client certificate in the process. /// </summary> /// <param name="requestUri">The Uri to post data.</param> /// <param name="postData">The data to be posted.</param> /// <param name="userName">The user account name (machinename\username) under which the client certificate was installed.</param> /// <param name="certPath">The path to the exported client certificate file.</param> /// <returns>The response from the remote host.</returns> /// <remarks> /// A client certificate is attached only if the Uri scheme is https and the client certificate path is supplied. /// It is assumed that if the certificate path is supplied, the certificate user account name is also supplied. /// </remarks> public string GetResponse( string requestUri, string postData, string userName, string certPath ) { TraceWriteLine( "HttpWebRequestor.GetResponse(string,string,string,string): Entered" );
try { ServicePointManager.CertificatePolicy = new AcceptAllCertificatePolicy( );
// Create the request HttpWebRequest request = ( HttpWebRequest ) WebRequest.Create( requestUri );
request.Method = "POST"; request.ContentType = "application/x-www-form-urlencoded"; request.ContentLength = postData.Length;
// Attach the client certificate if https and certPath specified. if ( new Uri( requestUri ).Scheme == Uri.UriSchemeHttps && certPath != "" ) { AttachClientCertificate( request, userName, certPath ); }
// Write data to request StreamWriter requestWriter = new StreamWriter( request.GetRequestStream( ) ); try { requestWriter.Write( postData ); } finally { requestWriter.Close( ); }
// Send to the remote server and wait for the response HttpWebResponse response = ( HttpWebResponse ) request.GetResponse( );
// Read the response string responseString; StreamReader responseReader = new StreamReader( response.GetResponseStream( ) ); try { responseString = responseReader.ReadToEnd( ); } finally { responseReader.Close( ); }
// Return response return responseString; } catch ( Exception e ) { throw; } finally { TraceWriteLine( "HttpWebRequestor.GetResponse(string,string,string,string): Exiting" );
LogMessage( _trace.ToString( ) ); }
}
By: Dave Scheele
Posted:
September 30, 2006 at 8:02 AM
I recently had the opportunity to attend the SharePoint Conference 2006 in Bellevue, Washington, a conference entirely dedicated to the next version of SharePoint, Microsoft Office SharePoint Server 2007. It was held in May at the Meydenbauer Center in the downtown area and featured lectures, demonstrations, and hands-on labs Monday through Thursday, and there was a partner-only migration planning workshop held on Friday.
Most of the sessions were extremely informative and had some really great demos. There were a few sessions I was disappointed with – for example, the workflow session was mostly lecture style with only one demo that showed building a very simple workflow using SharePoint designer. It was stated, but not demonstrated, that you can build workflows using Microsoft Visual Studio .NET. I got the feeling the workflow stuff wasn’t as fully developed and stable as the other features simply because of the lack of substantive demos.
A big push during the event was to use SharePoint as the foundation for building collaborative applications, rather than building them from scratch. Most workplace applications involve some type of data input which is then routed and/or transformed according to business rules, and of course saved to a data store. Using InfoPath forms, Windows Workflow Foundation, the SharePoint list user interface, and other out of the box functionality as high-level components which you glue together using VS.Net code (and for simpler applications no code at all), you can build applications far more quickly than if you built everything from scratch in ASP.NET.
My overall impression is that this is an extremely important software release for Microsoft, as they have obviously spent a huge amount of time and money on improving the product. It always felt to me like much of SharePoint Portal Server 2003 functionality was half-baked and the product as a whole, although extremely useful, was inconsistent in design and implementation. Microsoft has taken a huge step forward with this new release and it now feels consistent and functionally well thought-out. It shall be interesting to see how this initial impression changes as I get more practical experience with it.
There is much more to discuss (considering I was able to attend only 1/3 of all the available sessions throughout the week since there were always 3 sessions running in parallel at any given time) and I have only touched briefly on the material I did learn. I plan on making more postings about specific functionality in the near future.
To wrap up, I learned some general lessons about attending a development conference, which I relate below.
How to Attend a Conference
- Plan out the sessions you will attend in advance of the conference. Typically a day is broken into time periods during which several sessions are taking place in parallel and you’ll need to figure out which ones are best given your interests, your organizations interests, and your present and future client requirements.
- Get to each session 15 minutes early in order to get the best seats and stake out / claim a spot near a power outlet if your battery is running low.
- Try to absorb higher-level information and associate what is being presented with existing knowledge rather than furiously trying to take down the slides. They will be forthcoming and are usually posted on the web far sooner than the conference DVDs mailed out weeks after the conference.
- Make use of any question/answer periods after lectures. Even better, try to get access to the presenter after the formal session is over - you’ll get a much more interactive discussion than asking a question at a microphone and getting a response.
- Make sure to use a heavy duty battery in your laptop and make sure it is fully charged every night
- Bring along an extra-heavy duty battery and make sure it is fully charged every night
- I saw a guy taking pictures of slides with a camera: a good idea if you absolutely, positively must have the slide information immediately. I kind of resented him, honestly, for having the ability to take away more information than me immediately from the conference.
By: Dave Scheele
Posted:
September 30, 2006 at 8:02 AM
In Oscar Meszar's recent weblog entry, Config Files in Office Add-ins, he describes the need to set up configuration information specific to a DLL. The "official" method of configuring a .NET application is to create a configuration file associated with the executable file (or the web application), but DLL-specific configuration is only possible with a bit of work. In his case, he chose to create a separate configation file parser.
I encountered the same problem on a client engagement. The main application supported a plug-in architecture and it was necessary to configure each plug-in individually. The pattern I chose was to create a publically visible class and a corresponding internal class derived from MarshalByRefObject. The publically visible class was responsible for creating a new AppDomain, loading and unwrapping the MarshalByRefObject type, and delegating to the internal class which actually implemented the functionality.
Here is some sample code. The publically visible class is called SyncProcessor. The corresponding internal class is SyncProcessorInternal. The method SyncProcessor::GetNewAppDomain creates the new AppDomain and points it to a configuration file whose name is based on the name of the DLL - DLLName.dll.config. The SyncProcessor::GetSPI method instantiates a SyncProcessorInternal in the new AppDomain and returns the instance to the caller.
public class SyncProcessor { public void ProcessSelectedLocations( int userCode ) { AppDomain ad = GetNewAppDomain( );
try { SyncProcessorInternal spi = GetSPI( ad ); spi.ProcessSelectedLocations( userCode ); } finally { AppDomain.Unload( ad ); } }
public void ProcessPendingActions( int userCode ) { AppDomain ad = GetNewAppDomain( );
try { SyncProcessorInternal spi = GetSPI( ad ); spi.ProcessPendingActions( userCode ); } finally { AppDomain.Unload( ad ); } }
public bool IsChildLocationLocked( string location ) { AppDomain ad = GetNewAppDomain( );
try { SyncProcessorInternal spi = GetSPI( ad ); return spi.IsChildLocationLocked( location ); } finally { AppDomain.Unload( ad ); } }
/// <summary> /// Creates a new AppDomain in which to execute synchronization. We are doing this in order to associate a configuration file with the DLL /// rather than the code base of the parent process. The configuration file will be MAR_Synchronizer.dll.config, and is located in the same /// directory as the MAR_Synchronizer.dll. /// </summary> /// <returns>The new AppDomain</returns> private AppDomain GetNewAppDomain( ) { // The confifiguration file shall be located in the same directory as the DLL, and named like xxx.dll.config. string configFile = Assembly.GetExecutingAssembly( ).CodeBase + ".config";
// Create a new AppDomain setup object and specify the configuration file name/location. AppDomainSetup ads = new AppDomainSetup(); ads.ConfigurationFile = configFile;
// Create the new AppDomain using the AppDomainSetup. AppDomain ad = AppDomain.CreateDomain( "SyncProcessorInternal", null, ads );
return ad; }
/// <summary> /// Creates an instance of the SyncProcessorInternal class in the specified AppDomain. /// </summary> /// <param name="ad">The AppDomain in which to create the class.</param> /// <returns>An instance of the SyncProcessorInternal class</returns> private SyncProcessorInternal GetSPI( AppDomain ad ) { string assemblyName = Assembly.GetExecutingAssembly( ).GetName( ).Name; string typeName = "MRT.SyncProcessorInternal";
return ( SyncProcessorInternal ) ad.CreateInstanceAndUnwrap( assemblyName, typeName ); } }
internal class SyncProcessorInternal : MarshalByRefObject { public void ProcessSelectedLocations( int userCode ) { // Implementation code goes here }
public void ProcessPendingActions( int userCode ) { // Implementation code goes here }
public bool IsChildLocationLocked( string location ) { // Implementation code goes here }
}
|
|
|
|
 |
|
|
|
Professional Services ManagerDave Scheele is the professional services manager for PointBridge. He has over 15 years of experience in IT consulting, working with clients in a variety of industries, including banking, communicatio... [more]
|
|
|
|
|
|
|