You can use the ChangeXML utility to create and test ChangeXML. The following sections show how to use the ChangeXML utility to create each kind of change defined in the ChangeList.XSD.
After you log on, additional features become available. You are now able to create ChangeXML.
Note: |
|---|
|
Ensure you have active projects where the signed-on user has assignments. If the user has no assignments, you cannot create a change. You can use the code sample in the UpdateStatus method topic to create a project with the current Windows user assigned to a task.
|
Procedure 2. Create a simple change
-
In the ChangeXML utility, click Clear XML. This clears any text that is in the Change XML box and creates a new ChangeXML document.
-
Click Build Change XML to open the Generate a Change dialog box.
Figure 2. Using the Generate a Change dialog box
When you click Assignment or Task, the Items Available for Update list is updated with valid selections from the server; the box below the list is updated with properties appropriate for either an assignment or task. For more information about the valid properties, see Supported Project Fields and Field Information for Statusing ChangeXML.
-
Select an item from Items Available for Update. If no items are available, assign the logged-on user some tasks and publish the project.
-
Select a property to update. Entry controls suitable for that data type appear.
Note: |
|---|
|
Do not choose Custom Field at this time. Custom fields are addressed in detail later in this article.
|
-
Enter the value you want, and then click Update XML.
The Generate a Change dialog box closes, and the Change XML box on the main application form is updated with the information you entered.
-
Ensure that Run Updates is selected, and then click the Evaluate XML button to send the update to the server.
The ChangeXML utility sends the XML to update the status. It then submits the changes to the manager. The next time the project manager views updates made to the project, the changes made in the ChangeXML are presented for approval.
You can take an alternative approach in your application. Instead of submitting the changes to the manager for approval, add a change to the XML by setting s_apid_update_needed to true. This allows the affected team member to submit the change from their My Tasks page in Project Web Access.
The ChangeXMLUtil solution performs many functions that you might also need to perform in your application. The following sections highlight areas of code that might prove useful in constructing your application.
Validate ChangeXML Against the ChangeList Schema
We recommend that you validate the XML before you submit it to the server. Incorrect XML could have unpredictable results. The server might catch an error, but it is better to catch and correct the error before the incorrect XML is sent to the server.
The ChangeList Schema is included in the ChangeXMLUtil project, and the build action is set to Embedded Resource.
To make the XML and IO functions accessible, using statements are added to the code.
using System.Xml;
using System.Xml.Schema;
using System.IO;
A class-level variable is defined to hold the XSD.
private static XmlSchema UpdateChangeListSchema = null;
The XSD is read in and kept handy for use in validation.
System.IO.Stream xsdStream = this.GetType().Assembly.GetManifestResourceStream(this.GetType().Namespace.ToString()
+ ".ChangeList.xsd");
UpdateChangeListSchema =
XmlSchema.Read(new XmlTextReader(xsdStream), null);
The XML text is loaded into an XML document object, where the XSD is set as a schema and is evaluated.
try
{
XmlDocument changeDoc = new XmlDocument();
changeDoc.Schemas.Add(UpdateChangeListSchema);
changeDoc.LoadXml(changeXml);
changeDoc.Validate(null);
txtResult.Text="Success!";
return true;
}
catch (XmlSchemaValidationException ex)
{
txtResult.Text = "ChangeList XML does not validate against the schema:\r\n" + ex.LineNumber + ":" + ex.LinePosition + " " + ex.Message;
return false;
}
catch (System.Xml.XmlException ex)
{
txtResult.Text = "ChangeList XML is not well-formed:\r\n" + ex.LineNumber + ":" + ex.LinePosition + " " + ex.Message;
return false;
}
catch (Exception ex)
{
txtResult.Text = "An error occured during XML validation:\r\n" + ex.Message;
return false;
}
Get a List of Valid Standard Properties for Update
The fields supported for update are a subset of all task and assignment properties defined in Project Server, as well as custom fields.
The standard fields are identified by an unsigned integer constant known as a Property ID (PID).
All assignment properties are defined in the AssnConstID class and begin with the s_apid_ prefix. The values all have the mask s_assn_mask (0x0F000000).
All task properties are defined in the TaskConstID class and begin with the prefix s_tpid_, and have the s_task_mask (0x0B000000) mask.
Not all of these fields are identified as valid for update through Statusing, however. Tables of identified fields can be found in Supported Project Fields and Field Information for Statusing ChangeXML.
In ChangeXMLUtil, the valid fields are listed as literals to load in the list boxes.
cboTaskPids.Items.Add(new PidDisplayItem(184549384,"Actual Work", ProjDataFormat.Work));
cboTaskPids.Items.Add(new PidDisplayItem(184549394,"Deadline", ProjDataFormat.Date));
cboTaskPids.Items.Add(new PidDisplayItem(184549407,"Remaining Duration", ProjDataFormat.Duration));
cboTaskPids.Items.Add(new PidDisplayItem(184549405,"Duration", ProjDataFormat.Duration));
cboTaskPids.Items.Add(new PidDisplayItem(184549387,"Finish", ProjDataFormat.Date));
cboTaskPids.Items.Add(new PidDisplayItem(184549403,"Task Name", ProjDataFormat.Text));
cboTaskPids.Items.Add(new PidDisplayItem(184549381,"Overtime Work", ProjDataFormat.Work));
cboTaskPids.Items.Add(new PidDisplayItem(184549410,"% complete", ProjDataFormat.Percentage));
cboTaskPids.Items.Add(new PidDisplayItem(184549411,"% work complete", ProjDataFormat.Percentage));
cboTaskPids.Items.Add(new PidDisplayItem(184549412,"Physical % Complete", ProjDataFormat.Percentage));
cboTaskPids.Items.Add(new PidDisplayItem(184549415,"Regular Work", ProjDataFormat.Work));
cboTaskPids.Items.Add(new PidDisplayItem(184549382,"Remaining Work", ProjDataFormat.Work));
cboTaskPids.Items.Add(new PidDisplayItem(184549383,"Remaining Overtime Work", ProjDataFormat.Work));
cboTaskPids.Items.Add(new PidDisplayItem(184549389,"Resume", ProjDataFormat.Date));
cboTaskPids.Items.Add(new PidDisplayItem(184549380,"Scheduled Work", ProjDataFormat.Work));
cboTaskPids.Items.Add(new PidDisplayItem(184549386,"Start", ProjDataFormat.Date));
cboAssnPids.Items.Add(new PidDisplayItem(251658257, "Actual Finish", ProjDataFormat.Date));
cboAssnPids.Items.Add(new PidDisplayItem(251658256, "Actual Start", ProjDataFormat.Date));
cboAssnPids.Items.Add(new PidDisplayItem(251658250, "Actual Work", ProjDataFormat.Work));
cboAssnPids.Items.Add(new PidDisplayItem(251658251, "Actual Overtime Work", ProjDataFormat.Work));
cboAssnPids.Items.Add(new PidDisplayItem(251658275, "Assignment Units (Work Resource)", ProjDataFormat.Percentage));
cboAssnPids.Items.Add(new PidDisplayItem(251658275, "Assignment Units (Material Resource)", ProjDataFormat.Count));
cboAssnPids.Items.Add(new PidDisplayItem(251658253, "Finish", ProjDataFormat.Date));
cboAssnPids.Items.Add(new PidDisplayItem(251658295, "Confirmed", ProjDataFormat.YesNo));
cboAssnPids.Items.Add(new PidDisplayItem(251658247, "Overtime Work", ProjDataFormat.Work));
cboAssnPids.Items.Add(new PidDisplayItem(251658274, "% Work Complete", ProjDataFormat.Percentage));
cboAssnPids.Items.Add(new PidDisplayItem(251658282, "Regular Work", ProjDataFormat.Work));
cboAssnPids.Items.Add(new PidDisplayItem(251658248, "Remaining Work ", ProjDataFormat.Work));
cboAssnPids.Items.Add(new PidDisplayItem(251658249, "Remaining Overtime Work", ProjDataFormat.Work));
cboAssnPids.Items.Add(new PidDisplayItem(251658246, "Work", ProjDataFormat.Work));
cboAssnPids.Items.Add(new PidDisplayItem(251658252, "Start", ProjDataFormat.Date));
cboAssnPids.Items.Add(new PidDisplayItem(251658287, "Comments", ProjDataFormat.Text));
cboAssnPids.Items.Add(new PidDisplayItem(251658286, "Update Needed", ProjDataFormat.YesNo));
cboAssnPids.Items.Add(new PidDisplayItem(1, "Custom Field", ProjDataFormat.CustomField));
Note: |
|---|
|
Custom Field is defined as a "magic" constant to trigger custom field handling in the list of assignment PIDs.
|
Get a List of Assignments or Tasks
You obtain the list of items available for update through the ReadStatus method. As noted in Statusing, statusing works only in the context of the current logged-on user. So, when StatusingUtils calls ReadStatus, it obtains items valid for only that user.
StatusingWebSvc.StatusingDataSet statusingDs = statusing.ReadStatus(Guid.Empty, DateTime.MinValue, DateTime.MaxValue);
The available items are returned in both the Assignments table and the Tasks table.
Get a List of Custom Fields
CustomFieldsUtils.cs wraps the WebSvcCustomFields namespace. Only assignment custom fields can be updated through statusing. Any task custom field or resource custom field is available at the assignment level. Any change to the custom field at the assignment level does not update the value at the parent task or resource level.
To get a list of the available resource or task custom fields, we must first define a filter. The filter details the columns to return and the search criteria.
Note: |
|---|
|
Use the Secondary Custom Field ID as the Custom Field ID in the XML. This indicates the assignment-level custom field rather than the parent-level resource or task custom field.
|
// Instantiate the dataset to retrieve the list of literals.
// Done primarily to ensure an absence of typographical errors.
CustomFieldsWebSvc.CustomFieldDataSet ds = new CustomFieldsWebSvc.CustomFieldDataSet();
PSLibrary.Filter filter = new PSLibrary.Filter();
filter.FilterTableName = ds.CustomFields.TableName;
// List the fields.
// No sort order is specified as it is done in the
// list box.
filter.Fields.Add(new PSLibrary.Filter.Field(filter.FilterTableName,
ds.CustomFields.MD_ENT_TYPE_UIDColumn.ColumnName,
PSLibrary.Filter.SortOrderTypeEnum.None));
// ! Important !
// The custom field ID to use is the secondary ID, as this
// implies the assignment level, rather than the parent level.
filter.Fields.Add(new PSLibrary.Filter.Field(filter.FilterTableName,
ds.CustomFields.MD_PROP_UID_SECONDARYColumn.ColumnName,
PSLibrary.Filter.SortOrderTypeEnum.None));
filter.Fields.Add(new PSLibrary.Filter.Field(filter.FilterTableName,
ds.CustomFields.MD_PROP_NAMEColumn.ColumnName,
PSLibrary.Filter.SortOrderTypeEnum.None));
filter.Fields.Add(new PSLibrary.Filter.Field(filter.FilterTableName,
ds.CustomFields.MD_PROP_TYPE_ENUMColumn.ColumnName,
PSLibrary.Filter.SortOrderTypeEnum.None));
filter.Fields.Add(new PSLibrary.Filter.Field(filter.FilterTableName,
ds.CustomFields.MD_LOOKUP_TABLE_UIDColumn.ColumnName,
PSLibrary.Filter.SortOrderTypeEnum.None));
filter.Fields.Add(new PSLibrary.Filter.Field(filter.FilterTableName,
ds.CustomFields.MD_PROP_DEFAULT_VALUEColumn.ColumnName,
PSLibrary.Filter.SortOrderTypeEnum.None));
filter.Fields.Add(new PSLibrary.Filter.Field(filter.FilterTableName,
ds.CustomFields.MD_PROP_MAX_VALUESColumn.ColumnName,
PSLibrary.Filter.SortOrderTypeEnum.None));
// Set the filter. We want only resource and task custom fields.
PSLibrary.Filter.IOperator[] fos = new PSLibrary.Filter.IOperator[2];
fos[0] = new PSLibrary.Filter.FieldOperator(PSLibrary.Filter.FieldOperationType.Equal,
ds.CustomFields.MD_ENT_TYPE_UIDColumn.ColumnName,
PSLibrary.EntityCollection.Entities.TaskEntity.UniqueId);
fos[1] = new PSLibrary.Filter.FieldOperator(PSLibrary.Filter.FieldOperationType.Equal,
ds.CustomFields.MD_ENT_TYPE_UIDColumn.ColumnName,
PSLibrary.EntityCollection.Entities.ResourceEntity.UniqueId);
// Set the logical operator
PSLibrary.Filter.LogicalOperator lo = new
PSLibrary.Filter.LogicalOperator(PSLibrary.Filter.LogicalOperationType.Or, fos);
filter.Criteria = lo;
Finally, we use that filter to retrieve the custom fields.
customFieldDs = customFields.ReadCustomFields(filter.GetXml(), false);
Get a List of Custom Field Lookup Table Values
Some custom fields obtain their values from a lookup table. You can get these lookup tables by using the WebSvcLookupTable namespace.
If a custom field references a lookup table, the unique ID of the lookup table can be found in the MD_LOOKUP_TABLE_UID property.
The table unique ID is then passed into the ReadLookupTablesByUids method to obtain the lookup table values.
LookupTableUtils contains the call to obtain the lookup table values.
ds = myLookupTableSvc.ReadLookupTablesByUids(new Guid[] { tableUid }, false, -1);
Note: |
|---|
|
Do not use the ReadLookupTables method to obtain lookup table values, as custom field management permissions are required.
|
Use the XML Schema Definition Tool to Create Canonical XML
The ChangeXML utility uses the XML Schema Definition Tool (Xsd.exe) capability to generate classes based on ChangeList.XSD. These classes generate ChangeXML when serialized.
The following procedure shows how to create those classes and generate XML from them.
Prerequisities
Procedure 7. Generate serializable classes based on ChangeList XSD
-
Open a Visual Studio Command Prompt window, and then change directories to the solution directory. Following is an example.
cd %USERPROFILE%\My Documents\Visual Studio 2005\Projects\SampleApp
-
Execute the XSD command to generate classes. Following is an example.
xsd ChangeList.xsd /classes /language:cs
-
Add the generated classes to your application.
-
Ensure that your solution is open, and it is not running.
-
On the Project menu, click Add Existing Item.
-
Select ChangeList.cs, and then click Add.
Note: |
|---|
|
The ChangeList.cs class file is positioned under the ChangeList.XSD in the Visual Studio Solution Explorer.
|
ChangeListWrapper.cs uses the classes to instantiate the objects. It is idiosyncratic to this application. We recommend that you create your own wrapper. You might want to modify the generated classes to make them easier to use. One common modification is to change an Array class to a more flexible collection class, such as an ArrayList. This has a disadvantage, however, in that the changes are lost if you regenerate the classes to support a new version of the XSD.
ChangeListWraper.cs defines a singleton changes variable to hold the class hierarchy.
static private Changes changes = null;
Then, it sets it when it needs to be initialized.
For every change, the program adds a new Proj element. Your application can combine multiple changes in one Proj node. The following section creates a project and adds a change to it.
ChangesProj proj = new ChangesProj();
proj.ID = projectId.ToString();
if (changes.Proj == null)
{
changes.Proj = new ChangesProj[] { proj };
}
else
{
ChangesProj[] projs = changes.Proj;
Array.Resize<ChangesProj>(ref projs, ((int)changes.Proj.Length + 1));
changes.Proj = projs;
changes.Proj.SetValue(proj, changes.Proj.Length - 1);
}
It then goes down the hierarchy to add the child nodes, depending on type.
if(isTask)
{
AddTaskChange(changes.Proj[changes.Proj.Length-1],changeItem, itemId, value);
}
else
{
AddAssnChange(changes.Proj[changes.Proj.Length - 1], changeItem, itemId, value,
cfDisplayItem, luDisplayItem, isPeriodChange, periodStart, periodEnd);
}
The task change is relatively straightforward, as shown in the following code example.
private static void AddTaskChange(ChangesProj proj, PidDisplayItem changeItem, Guid itemId, string value)
{
// To be robust, do not assume task is empty.
if (null == proj.Task)
{
proj.Task = new ChangesProjTask[1];
}
else
{
ChangesProjTask[] tasks = proj.Task;
Array.Resize<ChangesProjTask>(ref tasks, ((int)tasks.Length + 1));
}
// Create the task and add it to the Proj element.
ChangesProjTask projTask = new ChangesProjTask();
proj.Task[proj.Task.Length - 1] = projTask;
// Fill in the properties.
projTask.ID = itemId.ToString();
// Create the change.
// Add it to the task.
// We'll just have the one child.
projTask.Change =new ChangesProjTaskChange[1];
ChangesProjTaskChange projTaskChange = new ChangesProjTaskChange();
projTask.Change[0] = projTaskChange;
// Fill in the properties.
projTaskChange.PID = changeItem.ValueMember;
projTaskChange.Value = value;
}
Because there are so many types, the assignment change requires more handling. If your application is not updating custom fields, or is not creating period changes, this can be simplified. The following code example shows the generic assignment change handling.
private static void AddAssnChange(ChangesProj proj, PidDisplayItem changeItem,
Guid itemId, string value, CFDisplayItem cfDisplayItem,
LookupTableDisplayItem ltDisplayItem, bool isPeriodChange,
DateTime periodStart, DateTime periodEnd){
// To be robust, do not assume assns is empty.
if (null == proj.Assn)
{
proj.Assn = new ChangesProjAssn[1];
}
else
{
ChangesProjAssn[] assns = proj.Assn;
Array.Resize<ChangesProjAssn>(ref assns, ((int)assns.Length + 1));
}
// Create the assignment.
ChangesProjAssn projAssn = new ChangesProjAssn();
// Add it to the project.
proj.Assn[proj.Assn.Length - 1] = projAssn;
// Fill in the properties.
projAssn.ID = itemId.ToString();
// We can assume that Items is empty, because
// we are doing everything else right here at once.
// Items is another word for change
projAssn.Items= new Object[1];
// Create the boxed typed change.
Object change = CreateTypedAssnChange(changeItem, value, cfDisplayItem, ltDisplayItem, isPeriodChange, ref periodStart, ref periodEnd);
projAssn.Items[0] = change;
}
Drilling down by change type necessitates the more detailed method to create the different assignment change types, and return it boxed.
private static Object CreateTypedAssnChange(PidDisplayItem changeItem,
string value, CFDisplayItem cfDisplayItem,
LookupTableDisplayItem ltDisplayItem, bool isPeriodChange,
ref DateTime periodStart, ref DateTime periodEnd)
{
Object change;
//Create the change based on type.
if (isPeriodChange)
{
if (changeItem.DataFormat == ProjDataFormat.Work)
{
ChangesProjAssnPeriodChange periodChange = new ChangesProjAssnPeriodChange();
periodChange.PID = changeItem.ValueMember;
periodChange.Start = Convert.ToDateTime(periodStart.ToString("yyyy-MM-ddTHH:mm:ss"));
periodChange.End = Convert.ToDateTime(periodEnd.ToString("yyyy-MM-ddTHH:mm:ss"));
periodChange.Value = value;
change = (object)periodChange;
}
else
{
change = null;
throw new Exception("Period changes are valid for Work data only.");
}
}
else
{
if ((ProjDataFormat)changeItem.DataFormat == ProjDataFormat.CustomField)
{
if (cfDisplayItem.HasLookup)
{
if (ltDisplayItem != null)
{
ChangesProjAssnLookupTableCustomFieldChange cfLuChange = new ChangesProjAssnLookupTableCustomFieldChange();
ChangesProjAssnLookupTableCustomFieldChangeLookupTableValue cfLuChangeValue = new ChangesProjAssnLookupTableCustomFieldChangeLookupTableValue();
cfLuChange.CustomFieldGuid = cfDisplayItem.ValueMember.ToString();
cfLuChange.CustomFieldName = cfDisplayItem.DisplayMember;
cfLuChange.CustomFieldType = getCustomFieldChageType(cfDisplayItem.DataType);
cfLuChange.IsMultiValued = cfDisplayItem.IsMultiValued;
cfLuChange.LookupTableValue = new ChangesProjAssnLookupTableCustomFieldChangeLookupTableValue[1];
cfLuChangeValue.Guid = ltDisplayItem.ValueMember.ToString();
cfLuChangeValue.Value = ltDisplayItem.BoxedValue.ToString();
cfLuChange.LookupTableValue[0] = cfLuChangeValue;
change = cfLuChange;
}
else
{
throw new MissingFieldException("No lookup value was specified. Please choose a lookup table item.", "Lookup Table Value");
}
}
else
{
ChangesProjAssnSimpleCustomFieldChange cfChange = new ChangesProjAssnSimpleCustomFieldChange();
cfChange.CustomFieldGuid = cfDisplayItem.ValueMember.ToString();
cfChange.CustomFieldName = cfDisplayItem.DisplayMember;
cfChange.CustomFieldType = getCustomFieldChageType(cfDisplayItem.DataType);
cfChange.Value = value;
change = cfChange;
}
}
else
{
ChangesProjAssnChange stdChange = new ChangesProjAssnChange();
stdChange.PID = changeItem.ValueMember;
stdChange.Value = value;
change = (Object)stdChange;
}
}
return change;
}
After the change is created, the application requests the XML in string form to update the ChangeXML box. ChangeListWrapper.cs serializes out the classes to a string for the main application.
Note: |
|---|
|
The UpdateStatus method fails without error if you include the XML definition tag. This procedure strips out the root XML tag.
|
static public string GetCurrentXmlAsString(bool OmitXmlTag)
{
XmlSerializer serializer = new XmlSerializer(typeof(Changes));
StringWriter writer = new StringWriter();
serializer.Serialize(writer, changes);
string xml = writer.ToString();
writer.Close();
if(OmitXmlTag)
{
xml = xml.Substring(xml.IndexOf("<Changes"));
}
return xml;
}
Send the Update to Project Server
After you have the XML string, the call to the UpdateStatus method is simple. ChangeXMLUtil has a wrapper class for the Statusing object.
statusing.UpdateStatus(changeXml);
Then, you must submit the status updates.
statusing.SubmitStatus(null, statusMsg);
As mentioned previously, you can take an alternative approach in your application. Instead of submitting the changes to the manager for approval with SubmitStatus, add a change to the XML by setting s_apid_update_needed to true. This allows the affected team member to submit the change from their My Tasks page in Project Web Access.
Handle Log On and Log Off
Log on and log off of the Web service wrappers are handled through the ProjSecureWebSvc base class. Any module that ends in Utils is a Project Server Web service wrapper.
LoginSource implements two interfaces, ILoginSvcConsumer and ILoginSvcController.
ILoginSvcController is used by the Log On form to control logging off and logging on the Project server.
ILoginSvcConsumer is used by the base class ProjSecureWebSvc and the main form to set state based on the log-on status.