Adding User Interface Enhancements
Applies to: Duet Enterprise for Microsoft SharePoint and SAP Server 2.0 | Office 2010 | SharePoint Server 2010
In this article
Improvements to the Solution
Enhancing the Sales Order Headers List Item Form Pages
Adding Data and Code to Support a Drop-Down List Box for Currency Selection
Modifying the Sales Order Header Visual Web Part
Simple Validation for Delivery Dates
Adding a Custom Action to the List Item Menu for the Sales Order Headers List
Invoking the New Item Form for Sales Order Items Based on a Specific Sales Order Header
Summary
After performing the steps in the previous topic, Developing the Primary Components for the Solution, you will have created a simple Duet Sales Order Management solution with some basic capabilities. In this topic, you will continue to develop the solution by making some improvements in its user interface.
Improvements to the Solution
The enhancements to the solution described in this topic represent only a small selection of the kinds of customizations you can do in a solution like the Duet Sales Order Management solution, but they will give you an idea of the development techniques available to you for solutions based on Duet Enterprise and SharePoint 2010. The improvements involve the following modifications to the solution:
Adding controls to the list item form pages for the Sales Order Headers List to facilitate data entry and provide simple validation.
Adding a custom action to the list item menu for headers in the Sales Order Headers List to display related sales order items.
Adding a link to the default view page associated with the Sales Order Items List to invoke the New Item form for sales order items based on a specific sales order header.
Enhancing the Sales Order Headers List Item Form Pages
As it is, the Visual Web Part on which the list item form pages for sales order headers is based is very simple. The editing controls on the Edit Item form (EditForm.aspx) for sales order headers, for example, are standard textbox controls.
Figure 1. Sales order headers Edit Item form before enhancements
.gif)
You can enhance this form by adding controls with available values for users to select when they edit sales order headers in SharePoint. As part of the process for developing the Duet Sales Order Management solution, you will first add a drop-down list box control with values available for selecting currencies for an order, and then you will add a DateTimeControl (one of the SharePoint Microsoft.SharePoint.WebControls) to allow users to pick dates from a calendar to specify delivery dates for an order.
Adding Data and Code to Support a Drop-Down List Box for Currency Selection
In a production-ready solution, you would probably generate a list of available currency types (such as dollars, pounds, or euros) based on the currency types in your SAP backend system. For the purposes of this walkthrough, you will simply create an XML file with a small selection of currency types to populate a SharePoint list.
In Visual Studio 2010, if the solution is not opened already, open the DuetSalesOrderSolution project you created in the previous topic (Developing the Primary Components for the Solution).
In Solution Explorer, select the DuetSalesOrderSolution project.
On the Project menu in Visual Studio, click New Folder. A new folder appears as a node in Solution Explorer. Specify "ListData" as the name of the folder and press ENTER.
Select the ListData folder created in the previous step.
On the Project menu, click Add New Item.
In the Add New Item dialog box, under Visual C# (or Visual Basic), click Data.
In the Templates pane, select XML File and specify "CurrencyData.xml" as the name of the file to be created.
Click Add. The XML file is added to the ListData folder in Solution Explorer and opened for editing.
Replace the contents of the file with the following markup:
<?xml version="1.0" encoding="utf-8" ?> <Currencies> <Currency Title="US Dollars" Identifier="USD" /> <Currency Title="Pounds" Identifier="GBP" /> <Currency Title="Rupees" Identifier="INR" /> <Currency Title="Euros" Identifier="EUR" /> </Currencies>The currencies specified here represent an arbitrary selection for the purposes of demonstration. You can specify different or additional currencies at your discretion.
Save the file.
On the Project menu, click DuetSalesOrderSolution Properties (or, alternatively, double-click the Properties node in Solution Explorer).
In the properties of the project, click the Resources tab.
If you haven't already added a resources file to the project, click the link on the resources property page to create one.
Press CTRL + 5 to switch to the Files resource type, and click Add Resource.
In the browsing window, specify All Files (*.*) as the file type, and navigate to the ListData folder in your project (that is, the folder you created in Step 4).
Double-click the CurrencyData.xml file. The file is added as an embedded resource to the default resources file for the project.
Save the resources file.
Next, you will develop a class that creates and provisions a SharePoint list for storing currency types, using the structured data from the CurrencyData.xml file.
In Solution Explorer, select the Customizations folder.
On the Project menu, click Add Class. The Add New Item dialog box appears with the class template already selected.
Specify "CurrencyList.cs" as the name of the class file.
Click Add. The file is added to the project and opened for editing.
Replace the contents of the file with the following code:
using System; using Microsoft.SharePoint; using System.Xml; namespace DuetSalesOrderSolution.Customizations { class CurrencyList { internal static string ListTitle = "CurrencyData"; internal static string[] ColumnNames = { "Title", "Identifier" }; internal void Activate(SPWeb spWeb) { const string listDescription = "Currency data for sales order forms."; // Check for the list and create one if it doesn't exist. SPList list = spWeb.Lists.TryGetList(ListTitle); if (list == null) { // Create new list and add columns. Guid listId = spWeb.Lists.Add(ListTitle, listDescription, SPListTemplateType.GenericList); list = spWeb.Lists[listId]; foreach (string columnName in ColumnNames) { if (columnName != ColumnNames[0]) { list.Fields.Add(columnName, SPFieldType.Text, true); } } // Set list properties for required scenario. list.OnQuickLaunch = false; list.EnableAttachments = false; list.EnableFolderCreation = false; list.ReadSecurity = 2; list.WriteSecurity = 4; // Allow only admins to update the list. list.Update(); } // If there is no data in the list already, add data from resources file. if (list.ItemCount == 0) { string currencyData = DuetSalesOrderSolution.Properties.Resources.CurrencyData; XmlDocument xmlDocument = new XmlDocument(); xmlDocument.LoadXml(currencyData); XmlNode currenciesNode = xmlDocument.FirstChild.NextSibling; foreach (XmlNode node in currenciesNode.ChildNodes) { SPListItem listItem = list.Items.Add(); foreach (string columnName in ColumnNames) { listItem[columnName] = node.Attributes[columnName].Value; } listItem.Update(); } } } } }Save the file.
The Activate method of this class creates a SharePoint list on a specified Web site if a list with the title "CurrencyData" doesn't already exist on the site. The list is then populated with data from the structured data file, CurrencyData.xml, created previously. You will now update the FeatureActivated method in the event receiver code for the Web-site level Feature of the solution to instantiate an object of the CurrencyList class and execute the Activate method of the class (when the Feature itself is activated on the site).
In Solution Explorer, under the Features node, select the SalesOrderSiteFeature Feature.
Press F7 to edit the event receiver code file (SalesOrderSiteFeature.EventReceiver.cs) for the Feature.
Replace the FeatureActivated method in the file with the following updated code:
public override void FeatureActivated(SPFeatureReceiverProperties properties) { base.FeatureActivated(properties); SPWeb spWeb = (SPWeb)properties.Feature.Parent); // Create Currency list. CurrencyList currencyList = new CurrencyList(); currencyList.Activate(spWeb); // Enable list customizations. ListCustomizations listCustomizations = new ListCustomizations(externalListTitles); listCustomizations.Activate(spWeb); }Save the file.
Modifying the Sales Order Header Visual Web Part
In the previous section, you created a class and some associated currency data to provide support for the addition to the Sales Order Header Visual Web Part of a drop-down list box that can facilitate the editing of headers by providing a list of available currencies to users. Now you will modify the markup that defines the user interface of the Sales Order Header Visual Web Part to add the drop-down list box for currencies. You will also add a SharePoint DateTimeControl to the Web Part in order to allow users to select delivery dates from a calendar rendered on the form.
In Solution Explorer, double-click the SalesOrderHeaderVisualWebPart project item. The control description file for the Web Part (SalesOrderHeaderVisualWebPartUserControl.ascx) is opened for editing.
Replace the entire ASP Table control (that is, everything from the <asp:Table> tag to the closing tag, </asp:Table>) with the following markup:
<asp:Table ID="Table1" runat="server"> <asp:TableRow> <asp:TableCell CssClass="ms-formlabel"> <SharePoint:FieldLabel runat="server" ID="FieldLabel1" FieldName="SalesOrderNumber" DisplaySize="5" /> </asp:TableCell> <asp:TableCell CssClass="ms-formbody"> <SharePoint:FormField runat="server" ID="ffldSalesOrderNumber" ControlMode="Display" FieldName="SalesOrderNumber" /> </asp:TableCell> <asp:TableCell CssClass="ms-formlabel"> <SharePoint:FieldLabel runat="server" ID="FieldLabel2" FieldName="PurchaseOrderNumber" DisplaySize="5" /> </asp:TableCell> <asp:TableCell CssClass="ms-formbody"> <SharePoint:FormField runat="server" ID="ffldPurchaseOrderNumber" FieldName="PurchaseOrderNumber" /> </asp:TableCell> </asp:TableRow> <asp:TableRow> <asp:TableCell CssClass="ms-formlabel"> <SharePoint:FieldLabel runat="server" ID="FieldLabel3" FieldName="SoldToParty" DisplaySize="5" /> </asp:TableCell> <asp:TableCell CssClass="ms-formbody"> <SharePoint:FormField runat="server" ID="ffldSoldToParty" FieldName="SoldToParty" /> </asp:TableCell> <asp:TableCell CssClass="ms-formlabel"> <SharePoint:FieldLabel runat="server" ID="FieldLabel4" FieldName="SalesOrganization" DisplaySize="5" /> </asp:TableCell> <asp:TableCell CssClass="ms-formbody"> <SharePoint:FormField runat="server" ID="ffldSalesOrganization" FieldName="SalesOrganization" /> </asp:TableCell> </asp:TableRow> <asp:TableRow> <asp:TableCell CssClass="ms-formlabel"> <SharePoint:FieldLabel runat="server" ID="FieldLabel5" FieldName="DeliveryDate" DisplaySize="5" /> </asp:TableCell> <asp:TableCell CssClass="ms-formbody"> <SharePoint:FormField runat="server" ID="ffldDeliveryDate" FieldName="DeliveryDate" /> <%--Adding DateTime Control to be displayed in Edit mode.--%> <SharePoint:DateTimeControl runat="server" ID="dateTimeDeliveryDate" DateOnly="true" /> </asp:TableCell> <asp:TableCell CssClass="ms-formlabel"> <SharePoint:FieldLabel runat="server" ID="FieldLabel6" FieldName="OrderType" DisplaySize="5" /> </asp:TableCell> <asp:TableCell CssClass="ms-formbody"> <SharePoint:FormField runat="server" ID="ffldOrderType" FieldName="OrderType" /> </asp:TableCell> </asp:TableRow> <asp:TableRow> <asp:TableCell CssClass="ms-formlabel"> <SharePoint:FieldLabel runat="server" ID="FieldLabel7" FieldName="Currency" DisplaySize="5" /> </asp:TableCell> <asp:TableCell CssClass="ms-formbody"> <SharePoint:FormField runat="server" ID="ffldCurrency" FieldName="Currency" /> <%--Adding drop-down list box for currencies to be displayed in Edit mode.--%> <asp:DropDownList runat="server" ID="dropDownCurrency" /> </asp:TableCell> <asp:TableCell CssClass="ms-formlabel"> <SharePoint:FieldLabel runat="server" ID="FieldLabel8" FieldName="NetValue" DisplaySize="5" /> </asp:TableCell> <asp:TableCell CssClass="ms-formbody"> <SharePoint:FormField runat="server" ID="ffldNetValue" ControlMode="Display" FieldName="NetValue" /> </asp:TableCell> </asp:TableRow> </asp:Table>Note that the only modifications to this markup are the additions of the SharePoint DateTimeControl and the ASP drop-down list box control. (You can simply add these new elements to the existing markup in this file instead of replacing the entire table element, but be sure to add them to the appropriate cells in the table, as indicated in this markup.)
Save the file.
With the addition of these new controls to the Sales Order Header Visual Web Part, the DeliveryDate field and the Currency field of the Sales Order Headers external content type are now associated with two controls each, one for simply displaying the data of the fields and one for editing that data. In an event handler for the list item form page that contains this Web Part, either the control for display or the control for editing will be presented to users, depending on whether the form is opened in display mode or not.
The next step is to add supporting routines, including the event handlers, to the code-behind file for the Sales Order Header Visual Web Part.
In Solution Explorer, under the node for the SalesOrderHeaderVisualWebPart item, select the control description file SalesOrderHeaderVisualWebPartUserControl.ascx.
Press F7 to open the associated code-behind file (SalesOrderHeaderVisualWebPartUserControl.ascx.cs) for editing.
Replace the contents of the file with the following code:
using System; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using Microsoft.SharePoint; using Microsoft.SharePoint.WebControls; using DuetSalesOrderSolution.Customizations; namespace DuetSalesOrderSolution.SalesOrderHeaderVisualWebPart { public partial class SalesOrderHeaderVisualWebPartUserControl : UserControl { public SalesOrderHeaderVisualWebPartUserControl() { this.PreRender += Page_PreRender; } protected void Page_Load(object sender, EventArgs e) { SPContext.Current.FormContext.OnSaveHandler += FormSaveHandler; PopulateCurrenciesDropDown(); // Prevent updating of read-only fields. if (SPContext.Current.FormContext.FormMode == SPControlMode.Edit) { ffldSalesOrderNumber.ControlMode = SPControlMode.Display; ffldNetValue.ControlMode = SPControlMode.Display; } } private void PopulateCurrenciesDropDown() { SPList list = SPContext.Current.Web.Lists.TryGetList(CurrencyList.ListTitle); if (list != null && dropDownCurrency.Items.Count != list.Items.Count) { dropDownCurrency.Items.Clear(); foreach (SPListItem listItem in list.Items) { ListItem item = new ListItem(listItem[CurrencyList.ColumnNames[0]].ToString(), listItem[CurrencyList.ColumnNames[1]].ToString()); dropDownCurrency.Items.Add(item); } } } private void MakeCurrencySelection() { if (ffldCurrency != null && ffldCurrency.Value != null && !String.IsNullOrEmpty(ffldCurrency.Value.ToString()) && ffldCurrency.Value.ToString() != dropDownCurrency.SelectedValue) { dropDownCurrency.SelectedValue = ffldCurrency.Value.ToString(); } } private void MakeDeliveryDateSelection() { if (ffldDeliveryDate != null && ffldDeliveryDate.Value != null && !String.IsNullOrEmpty(ffldDeliveryDate.Value.ToString()) && ffldDeliveryDate.Value.ToString() != dateTimeDeliveryDate.SelectedDate.ToString()) { DateTime dateTime = DateTime.Parse(ffldDeliveryDate.Value.ToString()); dateTimeDeliveryDate.SelectedDate = dateTime; } } protected void Page_PreRender(object sender, EventArgs e) { if (!IsPostBack) { if (SPContext.Current.FormContext.FormMode == SPControlMode.Edit) { MakeCurrencySelection(); MakeDeliveryDateSelection(); } // Control visibility for drop downs and the corresponding fields. if (SPContext.Current.FormContext.FormMode == Microsoft.SharePoint.WebControls.SPControlMode.Display) { dropDownCurrency.Visible = false; dateTimeDeliveryDate.Visible = false; } else { ffldDeliveryDate.Visible = false; ffldCurrency.Visible = false; } } } private void FormSaveHandler(object sender, EventArgs args) { SPContext.Current.Item["Currency"] = dropDownCurrency.SelectedValue; SPContext.Current.Item["DeliveryDate"] = dateTimeDeliveryDate.SelectedDate.ToString(); SaveButton.SaveItem(SPContext.Current, false, String.Empty); } } }Save the file.
In the updated FeatureActivated event handler of the SalesOrderSiteFeature Feature, a SharePoint list that stores currency types is now deployed to the target site when the Feature is activated. After this list is deployed to the site, users can update the list, adding or deleting values as with any standard list on a SharePoint site. In the code-behind file for the Sales Order Headers Visual Web Part, the PopulateCurrenciesDropDown method is called when the list item form page is loaded. This method attempts to retrieve the SharePoint list that stores currency types and, if that list is available, synchronizes the items in the currency drop-down list box on the form with the items in the SharePoint list of currencies. Then, at a later stage in the life-cycle of the list item form page, the Page_PreRender event handler is used to initialize the currency drop-down list box control and the DateTimeControl with the appropriate values from a given sales order header. The IsPostBack property of the control is checked to see if the control is being loaded for the first time (rather than as the result of a client postback operation) to determine whether the controls should be initialized. The Page_PreRender event handler also toggles the visibility of the controls associated with the DeliveryDate and Currency fields, depending on whether the form page has been loaded to display or edit data.
If you were to deploy this solution to your target SharePoint site, you would be able to bring up the Edit Item form for sales order headers to see the added controls (Figure 2).
Figure 2. Enhanced Edit Item form for sales order headers
.gif)
If you open the View Item form (DispForm.aspx) for an individual sales order header, the DateTime control and the currency drop-down list box control are hidden, and the SharePoint:FormField controls are displayed instead for the DeliveryDate and Currency fields, as in Figure 1.
Note
If the SharePoint DateTimeControl is rendered on your site without styles, it might be necessary to add a reference to the standard stylesheet for the control (datepicker.css) to the iframe.aspx page on your server (in %Program Files%\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\LAYOUTS on a standard SharePoint installation). If this is necessary, make a backup copy of the iframe.aspx file. Open the original file using a text editor such as Notepad.exe, and add the following stylesheet reference link between the opening and closing tags of the HEAD element:
<link rel="stylesheet" type="text/css" href="/_layouts/[LCID]/styles/datepicker.css"/>
Substitute the appropriate LCID value for "[LCID]" here, depending on the language of your installation. The LCID for English is 1033.
Simple Validation for Delivery Dates
There is very little validation of entered values in the Duet Sales Order Management solution. In a solution intended for actual production use, you would likely include various mechanisms to validate user-supplied data based on the circumstances of your environment and your specific business needs. As an example of one of the mechanisms available to you for implementing validation, the Duet Sales Order Management solution includes some simple validation logic related to delivery dates for sales orders. In the previous section, you added a DateTimeControl to the list item form page for sales order headers to facilitate editing delivery dates. The DateTimeControl includes some default validation, to ensure that users enter valid date information (rather than, say, text characters or other data). For the Duet Sales Order Management solution, additional validation is implemented to ensure that the delivery date specified is later than the current date.
To add code that implements this validation, once again you will edit the Sales Order Header Visual Web Part.
In Solution Explorer, double-click the SalesOrderHeaderVisualWebPart project item. The control description file for the Web Part (SalesOrderHeaderVisualWebPartUserControl.ascx) is opened for editing.
At the end of the file, after the closing tag (</asp:Table>) of the element that defines the ASP Table, add the following script:
<script language="ecmascript" type="text/ecmascript"> function PreSaveAction() { // Validate that delivery date is in the future. return CheckDeliveryDate("<%=dateTimeDeliveryDate.ClientID%>"); } function checkDeliveryDate(identifier) { var deliveryDateElement = getElementByTagAndId('INPUT', identifier); if (deliveryDateElement.value == null || deliveryDateElement.value == "") { alert('Delivery Date cannot be left blank.'); return false; } else { var now = new Date(); var enteredDate = Date.parse(deliveryDateElement.value); if (enteredDate < now) { alert('The specified Delivery Date must be in the future.'); return false; } } return true; } function getElementByTagAndId(tagName, identifier) { var inputTagElements = document.getElementsByTagName(tagName); var requiredElement; for (var i = 0; i < inputTagElements.length; i++) { if (inputTagElements[i].id.indexOf(identifier) != -1) { requiredElement = inputTagElements[i]; break; } } return requiredElement; } </script>Save the file.
In the code of the list item form pages for editing or creating items (that is, the EditForm.aspx and NewForm.aspx pages) as they are provisioned by SharePoint, the onclick even handler for the Save button on the form first checks the return value of the PreSaveItem function, which is defined in the form.js file that is automatically linked by SharePoint to these form pages. The onclick function executes a postback (save) operation only if the PreSaveItem function returns a value of "true". The PreSaveItem function itself determines whether another function, the PreSaveAction function, is defined anywhere in the script for the page. If so, the PreSaveItem function returns the value resulting from the execution of the PreSaveAction function. What this means is that by adding a PreSaveAction function to the page (and you can add only one implementation of the function to a page), you can prevent the save operation from proceeding or allow it to proceed, depending on the conditions you specify in your implementation of the PreSaveAction function. There is no default implementation of the PreSaveAction function in the scripts that are included in a SharePoint page (such as form.js). A SharePoint developer can implement the function based on the circumstances and requirements of a specific solution.
In the implementation of the PreSaveAction function here, a helper function, CheckDeliveryDate, queries the value of the DateTimeControl on the form. (The ClientID property, whose value is generated by SharePoint and varies for each deployment, is used as the identifier for the control.) If the value of the DateTimeControl is blank or if the value is a date that is earlier than the current date, the user is alerted and the function returns a value of "false" to the PreSaveAction function; otherwise, a value of "true" is returned. The PreSaveAction function passes the value, whichever it is, to the PreSaveItem function, and the save operation is accordingly aborted or allowed to proceed.
Adding a Custom Action to the List Item Menu for the Sales Order Headers List
The next enhancement to the Duet Sales Order Management solution is the addition of a custom action to the list item menu for headers in the Sales Order Headers List. The list item menu for a SharePoint list is the context menu that is associated with individual items in a list. This menu is also called an Edit Control Block (ECB) menu. The menu is rendered by SharePoint using script in the core.js file (located in %Program Files%\Common Files\Microsoft Shared\web server extensions\14\TEMPLATE\LAYOUTS\[LCID] in a standard SharePoint installation, where [LCID] corresponds to the installation language of your server, such as 1033 for English). In the Sales Order Headers List, this menu has three commands (or actions), based on the methods available from the Sales Order Header external content type.
Figure 3. List item menu for sales order headers
.gif)
There are several ways to add commands to a list item menu in SharePoint. For the Duet Sales Order Management solution, you will use script to insert a command into the menu. The script is integrated into the default list view Web Part Page for the Sales Order Headers List by setting the ContentLink property of a ContentEditorWebPart (CEWP) object. The CEWP itself is added to the default list view page when the SalesOrderSiteFeature is activated after the solution is deployed to your target SharePoint site.
First, you need to add a script file to your solution and make it available for use on your target SharePoint site. You will deploy the script to your site using a SharePoint project item in Visual Studio called a Module.
In Solution Explorer, select the DuetSalesOrderSolution project.
On the Project menu, click Add New Item. The Add New Item dialog box appears.
In the Add New Item dialog box, expand the SharePoint node, and then select the 2010 node.
In the list of templates in the Templates pane, select Module and specify "SalesOrderModule" as the name of the item.
Click Add. The module is added to the project, along with an Elements.xml file and a Sample.txt file, and the Elements.xml file is opened for editing.
The Sample.txt file is included in all new modules as an example and is not needed. (Note that deleting the sample file also removes the entry for the file from the Elements.xml file in the module.)
Select the SalesOrderModule module.
On the Project menu, click Add New Item.
In the Add New Item dialog box, select the Web node under Visual C# (or Visual Basic).
Select HTML Page in the Templates pane, and specify "SalesOrderHeaderScript.htm" as the name of the page.
For the Duet Sales Order Management solution, HTML pages are used as script container files, but you could also JScript files for this purpose, or even text files.
Click Add. The page is opened for editing.
Replace the contents of the file with the following script:
<script language="ecmascript" type="text/ecmascript"> // List of header items obtained by querying the Sales Order Headers list. var headerItems; // This function adds custom menu items to ECB. function Custom_AddListMenuItems(m, ctx) { CAMOpt(m, "Show Related Sales Order Items", "OpenSalesItemsForHeader('" + ctx.ListTitle + "','" + currentItemID + "');", "~/_layouts/images/DETAIL.gif"); CAMSep(m); return false; // Render the default menu items, too. } // This method queries the Headers list to obtain a SalesOrderSclKey for the item on which ECB menu has been invoked. function openSalesItemsForHeader(listTitle, itemId) { var viewXml = '<View><Query><Where><Eq><FieldRef Name="BdcIdentity" /><Value Type="Text">' + itemId + '</Value></Eq></Where></Query><ViewFields><FieldRef Name="BdcIdentity" /><FieldRef Name="SalesOrderSclKey"/></ViewFields></View>'; var ctx = new SP.ClientContext.get_current(); var web = ctx.get_web(); var list = web.get_lists().getByTitle(listTitle); var camlQuery = new SP.CamlQuery(); camlQuery.set_viewXml(viewXml); headerItems = list.getItems(camlQuery); ctx.load(headerItems, 'Include(BdcIdentity, SalesOrderSclKey)'); ctx.executeQueryAsync(Function.createDelegate(this, openSalesItemsForHeaderSuccess), Function.createDelegate(this, openSalesItemsForHeaderFail)); } function openSalesItemsForHeaderFail(sender, args) { alert('Failed to open sales order items: ' + args.get_message()); } function openSalesItemsForHeaderSuccess(sender, args) { var headerEnumerator = headerItems.getEnumerator(); var currentHeader; // There will be one and only one. while (headerEnumerator.moveNext()) { currentHeader = headerEnumerator.get_current(); break; } // Open the page using a URL with the required query string. var pageurl = L_Menu_BaseUrl + "/Lists/SalesOrderItems/SalesOrderItems.aspx?FilterField1=SalesOrderSclKey&FilterValue1=" + currentHeader.get_item("SalesOrderSclKey"); window.location.href = pageurl; } </script>Save the file.
Note that the module created in the foregoing steps, SalesOrderModule, and its associated files have been automatically added by Visual Studio to the site-level Feature for the solution, SalesOrderSiteFeature. The module will be deployed to the target SharePoint site specified in the Site URL property of the project. (For more information about using modules to deploy files in SharePoint, see the topic Using Modules to Include Files in the Solution in the MSDN Library.)
In the script that is added to the HTML page here, you are able to take advantage of an extensibility mechanism for list item menus that is similar in design to the PreSaveAction function used in the previous section for validation. In the core.js file in SharePoint, list item menus for lists (other than document libraries) are built by a function called AddListMenuItems. This function, before it adds the default commands to the list item menu, determines whether another function called Custom_AddListMenuItems is defined anywhere in the script for a given page. If that function is defined, it is executed first, and, depending on the value returned from that function, the AddListMenuItems function continues execution to add the default commands. If a given implementation of Custom_AddListMenuItems returns a value of "true", then AddListMenuItems discontinues execution and the list item menu will contain only those commands added in Custom_AddListMenuItems. For this solution, the custom function returns a value of "false" so that AddListMenuItems continues and adds the default commands to the menu in addition to a custom command. As with the PreSaveAction function discussed in the previous section, there is no default implementation of Custom_AddListMenuItems. The mechanism is available for SharePoint developers to be able to build custom list item menus according to the requirements of their specific solutions, bypassing the addition of default menu commands if necessary for a given design or, as in this solution, adding custom commands to the menu along with the default commands.
In the implementation of Custom_AddListMenuItems for this solution, a menu command is added using the CAMOpt function, which is also defined in core.js and is the same function used by the AddListMenuItems function in SharePoint to build list item menus. The CAMOpt function takes as its parameters an object that represents the list item menu to which the command will be added (m), the text to display for the command, a script function that performs the required action, and an optional URL of an image file to display along with the command on the menu. The variables used here (that is, m, ctx, currentItemID) are defined and initialized in core.js or init.js. The OpenSalesItemsForHeader function determines the SalesOrderSclKey value of a header in the Sales Order Headers List based on a BdcIdentity value. In SharePoint, items in external lists are identified and distinguished based on BdcIdentity values generated within SharePoint, but in the data models for the Duet Sales Order Management solution, sales order items are related to sales order headers based on SalesOrderSclKey values, so in order to find those sales order items that are related to a given header, the SalesOrderSclKey value must be established. When this value is established for the particular sales order header on which the list item menu has been invoked, the OpenSalesItemsForHeaderSuccess function builds a URL with a query string that includes the SalesOrderSclKey value as the basis for a filtered view. The function then uses that URL to open the default list view Web page for the Sales Order Items List such that the list is filtered to show only those sales order items that include a matching SalesOrderSclKey value.
Next, you will add methods to the ListCustomizations class in order to add the script in SalesOrderHeaderScript.htm to the default list view Web page for the Sales Order Headers List.
In Solution Explorer, select the ListCustomizations.cs file in the Customizations folder and press F7 to open the file for editing.
Add the following methods to the ListCustomizations class in the file:
private void AddCustomScriptToViewPage(SPWeb spWeb, string viewPageUrl, string scriptPageUrl) { SPFile file = spWeb.GetFile(viewPageUrl); using (SPLimitedWebPartManager webPartManager = file.GetLimitedWebPartManager(PersonalizationScope.Shared)) { ContentEditorWebPart contentEditorWebPart = new ContentEditorWebPart(); contentEditorWebPart.AllowEdit = false; contentEditorWebPart.ContentLink = SPUrlUtility.CombineUrl(spWeb.Url, scriptPageUrl); webPartManager.AddWebPart(contentEditorWebPart, "ZoneLeft", 0); } file.Update(); } private void AddCustomScriptToSalesOrderHeadersListPage(SPWeb spWeb) { SPList externalList = spWeb.Lists.TryGetList(externalListTitles[0]); if (externalList != null) { AddCustomScriptToViewPage(spWeb, externalList.DefaultViewUrl, "/SalesOrderModule/SalesOrderHeaderScript.htm"); } }Replace the Activate method in the class with the following updated code for the method:
public void Activate(SPWeb spWeb) { if (externalListTitles != null) { MakeExternalListsVisibleOnQuickLaunch(spWeb); AddCustomScriptToSalesOrderHeadersListPage(spWeb); } }Save the file.
The AddCustomScriptToViewPage method here adds a Content Editor Web Part to the specified Web page. If you are not creating custom Application Pages or Site Pages for your solution—in which case you could simply add a ScriptLink control to the page—the CEWP is a useful alternative mechanism for adding script to a page. In a SharePoint farm solution, you can get a reference to the Web Part Manager on a given Web Part Page and add the CEWP at runtime. (The SPLimitedWebPartManager object is not accessible in the more limited SharePoint object model available to sandboxed solutions.) Then, you can set the ContentLink property of the CEWP to point to a file containing the script you want to add to the page. In this way, the script is added only to those designated pages on your site to which you add the CEWP, not to all site pages (as would be the case if you were to deploy, for example, a CustomAction in a SharePoint Feature). In the ListCustomizations class for this solution, the CEWP is added to the default list view Web Part Page for the Sales Order Headers List and the ContentLink property of the CEWP is set to be the URL of the SalesOrderHeaderScript.htm file (based on the location to which it is deployed on the SharePoint site in the SalesOrderModule module).
If you were to deploy the solution, the Sales Order Headers List would now have customized list item menus for headers, with an additional command, shown in Figure 4.
Figure 4. List Item Menu with custom action command
.gif)
This command displays the default view of the Sales Order Items List, filtered based on a particular sales order header (that is, based on a given SalesOrderSclKey value, which is the value on which sales order headers and items are related to each other). In the next section, you will add the ability to invoke the New Item form for sales order items based on this filtered list (that is, already populated with the appropriate SalesOrderSclKey value), so that a new sales order item added using the form would be related to a particular sales order header.
Invoking the New Item Form for Sales Order Items Based on a Specific Sales Order Header
For the final enhancement to the user interface of the Duet Sales Order Management solution, you will add a link to the default list view Web Part Page for sales order items that can be used to bring up the New Item form (NewForm.aspx) for sales order items with a SalesOrderSclKey value (which relates sales order items to headers) already specified in the form.
First, an additional control must be added to the markup for the Sales Order Item Visual Web Part.
In Solution Explorer, double-click the SalesOrderItemVisualWebPart project item. The control description file for the Web Part (SalesOrderItemVisualWebPartUserControl.ascx) is opened for editing.
Replace the entire ASP Table control (that is, everything from the <asp:Table> tag to the closing tag, </asp:Table>) with the following markup:
<asp:Table ID="Table1" runat="server"> <asp:TableRow> <asp:TableCell CssClass="ms-formlabel"> <SharePoint:FieldLabel runat="server" ID="FieldLabel1" FieldName="ItemNumber" DisplaySize="5" /> </asp:TableCell> <asp:TableCell CssClass="ms-formbody"> <SharePoint:FormField runat="server" ID="ffldItemNumber" FieldName="ItemNumber" /> </asp:TableCell> </asp:TableRow> <asp:TableRow> <asp:TableCell CssClass="ms-formlabel"> <SharePoint:FieldLabel runat="server" ID="FieldLabel2" FieldName="Sales Order Header" DisplaySize="5" /> </asp:TableCell> <asp:TableCell CssClass="ms-formbody"> <SharePoint:FormField runat="server" ID="ffldSalesOrderSclKey" FieldName="Sales Order Header" /> <asp:TextBox ID="computedSalesOrderHeader" runat="server" Visible="false" ReadOnly="true" CssClass="ms-long"></asp:TextBox> </asp:TableCell> </asp:TableRow> <asp:TableRow> <asp:TableCell CssClass="ms-formlabel"> <SharePoint:FieldLabel runat="server" ID="FieldLabel3" FieldName="Product" DisplaySize="5"/> </asp:TableCell> <asp:TableCell CssClass="ms-formbody"> <SharePoint:FormField runat="server" ID="ffldProduct" FieldName="Product" /> </asp:TableCell> </asp:TableRow> <asp:TableRow> <asp:TableCell CssClass="ms-formlabel"> <SharePoint:FieldLabel runat="server" ID="FieldLabel4" FieldName="MaterialShortText" DisplaySize="5" /> </asp:TableCell> <asp:TableCell CssClass="ms-formbody"> <SharePoint:FormField runat="server" ID="ffldMaterialShortText" FieldName="MaterialShortText" /> </asp:TableCell> </asp:TableRow> <asp:TableRow> <asp:TableCell CssClass="ms-formlabel"> <SharePoint:FieldLabel runat="server" ID="FieldLabel5" FieldName="Quantity" DisplaySize="5" /> </asp:TableCell> <asp:TableCell CssClass="ms-formbody"> <SharePoint:FormField runat="server" ID="ffldQuantity" FieldName="Quantity" /> </asp:TableCell> </asp:TableRow> <asp:TableRow> <asp:TableCell CssClass="ms-formlabel"> <SharePoint:FieldLabel runat="server" ID="FieldLabel6" FieldName="SalesUnit" DisplaySize="5" /> </asp:TableCell> <asp:TableCell CssClass="ms-formbody"> <SharePoint:FormField runat="server" ID="ffldSalesUnit" FieldName="SalesUnit" /> </asp:TableCell> </asp:TableRow> <asp:TableRow> <asp:TableCell CssClass="ms-formlabel"> <SharePoint:FieldLabel runat="server" ID="FieldLabel7" FieldName="NetPrice" DisplaySize="5" /> </asp:TableCell> <asp:TableCell CssClass="ms-formbody"> <SharePoint:FormField runat="server" ID="ffldNetPrice" FieldName="NetPrice" /> </asp:TableCell> </asp:TableRow> <asp:TableRow> <asp:TableCell CssClass="ms-formlabel"> <SharePoint:FieldLabel runat="server" ID="FieldLabel8" FieldName="Currency" DisplaySize="5" /> </asp:TableCell> <asp:TableCell CssClass="ms-formbody"> <SharePoint:FormField runat="server" ID="ffldCurrency" FieldName="Currency" /> </asp:TableCell> </asp:TableRow> <asp:TableRow> <asp:TableCell CssClass="ms-formlabel"> <SharePoint:FieldLabel runat="server" ID="FieldLabel9" FieldName="NetValue" DisplaySize="5" /> </asp:TableCell> <asp:TableCell CssClass="ms-formbody"> <SharePoint:FormField runat="server" ID="ffldNetValue" FieldName="NetValue" /> </asp:TableCell> </asp:TableRow> <asp:TableRow> <asp:TableCell CssClass="ms-formlabel"> <SharePoint:FieldLabel runat="server" ID="FieldLabel10" FieldName="Plant" DisplaySize="5" /> </asp:TableCell> <asp:TableCell CssClass="ms-formbody"> <SharePoint:FormField runat="server" ID="ffldPlant" FieldName="Plant" /> </asp:TableCell> </asp:TableRow> </asp:Table>Note that the only modification to this markup is the addition of the ASP TextBox control. (Again, you can simply add this control to the existing markup in this file instead of replacing the entire table element, but be sure to add it to the appropriate cell in the table, as indicated in this markup.)
Save the file.
Now, in the list item form pages for sales order items, the SalesOrderSclKey value for a given item (which relates the item to a particular sales order header) is associated with two editing controls, a SharePoint:FormField control and an ASP TextBox control. In the following steps, you will update the code-behind file for the Sales Order Item Visual Web Part to use one control or the other for editing the SalesOrderSclKey value, depending on how the New Item form for a sales order item is invoked.
In Solution Explorer, under the SalesOrderItemVisualWebPart project item, select the control description file for the Web Part (SalesOrderItemVisualWebPartUserControl.ascx).
Press F7 to open the code-behind file for SalesOrderItemVisualWebPart (SalesOrderItemVisualWebPartUserControl.ascx.cs).
Replace the contents of the file with the following code:
using System; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using Microsoft.SharePoint; using Microsoft.SharePoint.WebControls; namespace DuetSalesOrderSolution.SalesOrderItemVisualWebPart { public partial class SalesOrderItemVisualWebPartUserControl : UserControl { protected void Page_Load(object sender, EventArgs e) { SPContext.Current.FormContext.OnSaveHandler += FormSaveHandler; // Prevent updating of read-only fields. if (SPContext.Current.FormContext.FormMode == SPControlMode.Edit) { ffldItemNumber.ControlMode = SPControlMode.Display; } // Auto populate the sales order header value when a related item is bing added if (!this.IsPostBack && SPContext.Current.FormContext.FormMode == SPControlMode.New) { MakeSalesOrderSclKeySelection(); } } /// <summary> /// Sets SalesOrderSclKey field value based on the value in query string. /// </summary> protected void MakeSalesOrderSclKeySelection() { string salesOrderKey = this.Page.Request.QueryString["SalesOrderSclKey"]; if (!String.IsNullOrEmpty(salesOrderKey)) { computedSalesOrderHeader.Visible = true; ffldSalesOrderSclKey.Visible = false; computedSalesOrderHeader.Text = salesOrderKey; } } private void FormSaveHandler(object sender, EventArgs args) { if (!ffldSalesOrderSclKey.Visible) { SPContext.Current.Item["SalesOrderSclKey"] = computedSalesOrderHeader.Text; } SaveButton.SaveItem(SPContext.Current, false, String.Empty); } } }Save the file.
The MakeSalesOrderSclKeySelection function in this code is straightforward. It looks for a SalesOrderSclKey value in the query string of the URL with which the form was invoked. The query string in a URL is everything after the question mark (?) in a URL such as the following (a sample SalesOrderSclKey value here is in bold type):
http://[SharePoint Server]/sites/[SharePoint Site]/DuetSalesOrders/Lists/SalesOrderItems/NewForm.aspx?RootFolder=&IsDlg=1&SalesOrderSclKey=1019_0000000174_ZSALES_ORDER_HEADER_E05_400&IsDlg=1
If the MakeSalesOrderSclKeySelection function obtains a SalesOrderSclKey value from the query string of the form URL, it toggles the visibility of the two controls in the form for editing the SalesOrderSclKey value, displaying the ASP TextBox control in place of the SharePoint:FormField control. Then the function sets the value of the Text property of the TextBox control to be the value found in the query string. This function is called in the Page_Load event handler only if the form is being loaded for the first time (that is, not as a result of a postback operation) and as a New Item form. In the FormSaveHandler routine, if the SharePoint:FormField control is not visible, the SalesOrderSclKey value of the sales order item being created is set to be the value of the text in the TextBox control.
Next you will add to the default list view page for sales order items a script with routines to build New Item form URLs with appropriate query strings. As in the previous section, you will add the script file to the SalesOrderModule module.
In Solution Explorer, select the SalesOrderModule module.
On the Project menu, click Add New Item.
In the Add New Item dialog box, select the Web node under Visual C# (or Visual Basic).
Select HTML Page in the Templates pane, and specify "SalesOrderItemScript.htm" as the name of the page.
Click Add. The page is opened for editing.
Replace the contents of the file with the following script:
<table id="idAddSalesOrderItem" width="100%" cellpadding="0" cellspacing="0" border="0"> <tr> <td colspan="2" class="ms-partline"> <img src="/_layouts/images/blank.gif" width="1" height="1" alt="" /> </td> </tr> <tr> <td class="ms-addnew" style="padding-bottom: 3px"> <span style="height:10px;width:10px;position:relative;display:inline-block;overflow:hidden;" class="s4-clust"> <img src="/_layouts/images/fgimg.png" alt="" style="left:-0px !important;top:-128px !important;position:absolute;" /> </span> <a class="ms-addnew" href="javascript:createSalesOrderItem()" target="_self">Add Related Sales Order Item</a> </td> </tr> </table> <script language="ecmascript" type="text/ecmascript"> // Get the query string for a key. function getQueryString(queryKey) { var queryString = window.location.search.substring(1); var queryPairs = queryString.split("&"); for (var i = 0; i < queryPairs.length; i++) { var queryPair = queryPairs[i].split("="); if (queryPair[0] == queryKey) { return queryPair[1]; } } return ""; } // Invokes New Item form with SalesOrderSclKey as query string. function createSalesOrderItem() { var siteUrl = SP.PageContextInfo.get_webServerRelativeUrl(); if (siteUrl == "/") { siteUrl = ""; } if (getQueryString("FilterField1") == "SalesOrderSclKey") { pageurl = siteUrl + "/Lists/SalesOrderItems/NewForm.aspx?RootFolder=&IsDlg=1&SalesOrderSclKey=" + getQueryString("FilterValue1"); } else { pageurl = siteUrl + "/Lists/SalesOrderItems/NewForm.aspx?RootFolder=IsDlg=1"; } var options = SP.UI.$create_DialogOptions(); options.url = pageurl; options.autoSize = true; options.dialogReturnValueCallback = Function.createDelegate(null, closeCallback); SP.UI.ModalDialog.showModalDialog(options); } function closeCallback(result, target) { if (result == SP.UI.DialogResult.OK) { window.location.reload(true); } } </script>Save the file.
The main function in this script, createSalesOrderItem, prepares to invoke the New Item form for sales order items by identifying the SalesOrderSclKey value on which the Sales Order Items List has been filtered (if the list is filtered). If the default list view page for the Sales Order Items List was opened using the custom action on the list item menu for sales order headers that you created in the previous section, then the URL of the page will contain a SalesOrderSclKey value (extracted here using the helper function getQueryString) that can be used in turn as part of the URL to invoke the New Item form. (If the Sales Order Items List is not filtered, the New Item form is opened without a SalesOrderSclKey value in its URL.) Then, as described earlier, the MakeSalesOrderSclKeySelection function you added to the code-behind file for the Sales Order Item Visual Web Part does its work to set the value of the appropriate field on the form to the value it finds in the form's URL.
The table element here, along with its contents, simply renders the link on the page such that is formatted in a way similar to the standard SharePoint Add New Item link for lists.
Finally, you need to add another method to the ListCustomizations class in order to add the script in SalesOrderItemScript.htm to the default list view Web page for sales order items when the solution is deployed.
In Solution Explorer, select the ListCustomizations.cs file in the Customizations folder and press F7 to open the file for editing.
Add the following method to the ListCustomization class in the file:
private void AddCustomScriptToSalesOrderItemsListPage(SPWeb spWeb) { SPList externalList = spWeb.Lists.TryGetList(externalListTitles[1]); if (externalList != null) { AddCustomScriptToViewPage(spWeb, externalList.DefaultViewUrl, "/SalesOrderModule/SalesOrderItemScript.htm"); } }Replace the Activate method in the class with the following updated code for the method:
public void Activate(SPWeb spWeb) { if (externalListTitles != null) { MakeExternalListsVisibleOnQuickLaunch(spWeb); AddCustomScriptToSalesOrderHeadersListPage(spWeb); AddCustomScriptToSalesOrderItemsListPage(spWeb); } }Save the file.
Note that the AddCustomScriptToSalesOrderItemsListPage function here is directly analogous to the AddCustomScriptToSalesOrderHeadersListPage function added in the previous section. In a production solution, you would probably combine these two functions into a single function and use parameter values to target either the Sales Order Headers List or Sales Order Items List. They are separated out here for the sake of clarity and because we have been developing the Duet Sales Order Management solution in stages over the course of this walkthrough.
Summary
If you deploy the solution to your target SharePoint site, you should be able to click the custom command on the list item menu for a given sales order header to bring up the default list view page for sales order items, and the list of items will be filtered based on the selected sales order header. The list view page for sales order items will also have a link (see Figure 5) that executes the createSalesOrderItem function to open the New Item form with the SalesOrderSclKey value of the selected sales order header already specified.
Figure 5. Link to open New Form to add a related sales order item
.gif)
In addition, the forms for editing sales order headers or creating new headers are now augmented with additional controls to facilitate data entry, and script that has been added to the markup for the forms includes simple validation of entered delivery dates.
In the next topic, Configuring and Activating Reporting, you will enable reporting capabilities for the Duet Sales Order Management solution.