SharePoint Calculator Service Part 6 – Custom Admin Setting
In Part 5 of this series, we created a PowerShell cmdlet to enable SharePoint administrators to script deployment of our Calculator service application in a server farm.
In this article, we’ll create a custom setting that administrators can use to modify the behavior of our service application.
Specifically, we’ll create a new Precision setting, which is defined as the number of significant digits that are used to represent the result of a calculation.
Create the setting
To do this, we’ll first add the setting to our CalculatorServiceApplication class.
You might think that it would be challenging to create a setting that can be managed from a single location and made available to all servers in a server farm via a distributed multi-tier cache, but that’s actually the easiest part thanks to the SharePoint configuration object model:
- internal sealed class CalculatorServiceApplication : SPIisWebServiceApplication
- {
- [Persisted]
- private int precision;
- public int Precision
- {
- get
- {
- return this.precision;
- }
- set
- {
- if ((value < 1) || (value > 20))
- {
- throw new ArgumentOutOfRangeException("value");
- }
- this.precision = value;
- }
- }
- }
The precision field (line 4) represents the current precision setting for the service application, and the Precision property (lines 6-22) implement a public property to get and set the field value.
The key to integrating with the SharePoint configuration object model is the [Persisted] attribute (line 3). This attribute instructs SharePoint to persist the field value in the SharePoint configuration database when the Update method is called, and to deserialize it when the class is read from the configuration database.
That’s all it takes for the field value to be available on all machines in the server farm and automatically cached on demand for runtime access!
NOTE: The Service Application Framework does not require service application settings to be stored in the SharePoint configuration database using the [Persisted] attribute described above. For example, if a service has an existing mechanism for storing settings, then that would be fine to use instead. However, you may find this persistence mechanism convenient in many cases.
Use the setting
Next, we’ll read this value at runtime and apply it to the result of our Add method:
- internal sealed class CalculatorServiceContract : ICalculatorServiceContract
- {
- public int Add(int a, int b)
- {
- return RoundToPrecision(a + b);
- }
- private CalculatorServiceApplication ServiceApplication
- {
- get
- {
- // Lookup executing service application in config db cache
- return (CalculatorServiceApplication)SPIisWebServiceApplication.Current;
- }
- }
- private int RoundToPrecision(int value)
- {
- // Get the current precision setting from the service application configuration
- int precision = this.ServiceApplication.Precision;
- // Quick-and-dirty round to specified precision
- return int.Parse(value.ToString("G" + precision.ToString()), System.Globalization.NumberStyles.Any);
- }
- }
First, we add a ServiceApplication property to look up and return the executing Calculator service application (lines 8-15).
NOTE: If we had implemented our contract as methods on the CalculatorServiceApplication, this step would be unnecessary, i.e., we could just use “this” to reference the current service application. However, in this example we’re assuming that we’re integrating an existing WCF service contract implementation with SharePoint.
Then, we get the precision setting from our service application using this.ServiceApplication.Precision (line 20) and use it to round our result (line 23).
NOTE: I couldn’t find a nice math function to round a number to an arbitrary precision, so I ended up converting the int to a string formatted to display the given precision and then converting the formatted string back to an int. Not something I’d recommend for production code.
It is important to note that the ServiceApplication lookup (line 13) and the Precision setting (line 2) objects are cached in memory by the SharePoint configuration object model and should not be cached again. This is a common implementation mistake and results in stale setting values. The reason is that SharePoint is smart about invalidating the cache and keeping it fresh when administrators make changes to settings, even from other machines in the server farm. If you cache the result of a lookup, the object will become stale and your application won’t see changes made by administrators until your service host process is recycled.
So, simply follow the pattern above and lookup the setting on every access to ensure that you are reading the most recent value.
Manage the setting
Now let’s provide a way for the administrator to configure the setting. To do this, we’ll integrate with the “Manage” ribbon button on the Service Applications management page:
First, we’ll create a management ASPX page:
ManageApplication.aspx
- <asp:content contentplaceholderid="PlaceHolderMain" runat="server">
- <table border="0" cellspacing="0" cellpadding="0" width="100%" class="ms-propertysheet">
- <wssuc:InputFormSection
- Title="Arithmetic Precision"
- runat="server">
- <Template_Description>
- <wssawc:EncodedLiteral runat="server" text="Arithmetic precision is the number of significant digits that are used to represent the result of a calculation." EncodeMethod='HtmlEncodeAllowSimpleTextFormatting'/>
- </Template_Description>
- <Template_InputFormControls>
- <wssuc:InputFormControl runat="server">
- <Template_LabelText>
- <wssawc:EncodedLiteral text="Precision:" EncodeMethod="HtmlEncode" runat="server"/>
- </Template_LabelText>
- <Template_control>
- <wssawc:InputFormTextBox id="PrecisionTextBox" name="PrecisionTextBox" title="Precision" MaxLength="2" Width="50" class="ms-input" runat="server" />
- <wssawc:EncodedLiteral text="digits" EncodeMethod="HtmlEncode" runat="server" />
- <wssawc:InputFormRangeValidator
- id="PrecisionValidator"
- Type="Integer"
- MinimumValue="1"
- MaximumValue="20"
- ControlToValidate="PrecisionTextBox"
- ErrorMessage="Specify an integer value in the range 1..20"
- runat="server" />
- </Template_control>
- </wssuc:InputFormControl>
- </Template_InputFormControls>
- </wssuc:InputFormSection>
- <wssuc:ButtonSection runat="server">
- <Template_Buttons>
- <asp:Button id="OkButton" OnClick="OkButton_Click" UseSubmitBehavior="false" Text="OK" class="ms-ButtonHeightWidth" runat="server" />
- </Template_Buttons>
- </wssuc:ButtonSection>
- </table>
- </asp:content>
NOTE: I removed a bunch of standard page template goo for readability.
Then, the code-behind:
ManageApplication.aspx.cs
- [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
- [PermissionSet(SecurityAction.InheritanceDemand, Name = "FullTrust")]
- public class ManageApplicationPage : GlobalAdminPageBase
- {
- protected TextBox PrecisionTextBox;
- protected override void OnLoad(EventArgs e)
- {
- base.OnLoad(e);
- if (!Page.IsPostBack)
- {
- CalculatorServiceApplication serviceApplication = this.ServiceApplication;
- if (null == serviceApplication)
- {
- throw new InvalidOperationException("Service application not found.");
- }
- // Initialize controls from service application
- this.PrecisionTextBox.Text = serviceApplication.Precision.ToString();
- }
- }
- private CalculatorServiceApplication ServiceApplication
- {
- get
- {
- // Lookup service application by querystring parameter
- Guid serviceApplicationGuid = new Guid(HttpContext.Current.Request.QueryString["id"]);
- return SPFarm.Local.GetObject(serviceApplicationGuid) as CalculatorServiceApplication;
- }
- }
- protected void OkButton_Click(object sender, EventArgs e)
- {
- if (Page.IsValid)
- {
- // Update service application settings
- CalculatorServiceApplication serviceApplication = this.ServiceApplication;
- serviceApplication.Precision = Int32.Parse(PrecisionTextBox.Text);
- serviceApplication.Update();
- // Navigate back to the service application management page
- RedirectOnOK();
- }
- }
- public override string PageToRedirectOnOK
- {
- get { return "/_admin/ServiceApplications.aspx"; }
- }
- public override string PageToRedirectOnCancel
- {
- get { return "/_admin/ServiceApplications.aspx"; }
- }
- }
The interesting bit of code here is the ServiceApplication property (lines 24-32). Here we’re assuming that the URL used to navigate to the aspx page will include a querystring parameter named “id” with a value that matches the Guid identifier of the service application to be managed. This is necessary because, if you remember our discussion on service application topologies, there may be more than one of our Calculator service applications in a server farm. We parse the Guid parameter and use it to look up the specified service application using the configuration object model (line 30).
And finally, we’ll hook our new management page up to the ribbon by overriding the ManageLink property of our service application:
- internal sealed class CalculatorServiceApplication : SPIisWebServiceApplication
- {
- public override SPAdministrationLink ManageLink
- {
- get
- {
- return new SPAdministrationLink("/_admin/sample/calculator/manageapplication.aspx?id=" + this.Id.ToString());
- }
- }
- }
Note that we include the “id” querystring parameter in the URL required by our management page (line 7).
That’s it!
We’ve added a configurable setting and a page to manage it:
Administrators can create multiple service applications and configure each of them with a different setting.
However, we don’t have a way for administrators to script this new setting using PowerShell. We’ll take care of that next time.