Friday, December 12, 2014

Modifying WrkStat.aspx With Custom TaskOutcome

Hello!

If you have ever developed custom TaskOutcome field for 2013 workflows with your own choices (link 1 or link 2), you have probably thought how to customize WrkStat.aspx and select your custom column in it for the tasks view instead of standard TaskOutcome:


It is possible to achieve that by modifying settings via browser and WrkStat.aspx file in filesystem, but it is not easy. May be in future we will know how to achieve that by modifying workflow definition (association).

We will see below that CAML does not filter list items by hidden fields. And that is really frustrating. But we will find solution.

Let's begin. First of all, you need to backup your WrkStat.aspx in layouts folder (15 hive). Then add imports:

    <%@ Import Namespace="System.Text" %>
    <%@ Import Namespace="System.IO" %>

Then add this code at the beginning of PlaceHolderMain (see my post about task list id):
   <script runat="server">  
        public Guid GetTaskListId(SPWeb web, Guid ListId, int ItemId, Guid workflowID)  
        {  
            WorkflowServicesManager workflowServiceManager = new WorkflowServicesManager(web);  
            var workflowInstanceService = workflowServiceManager.GetWorkflowInstanceService();  
            WorkflowInstance workflowInstance = workflowInstanceService.EnumerateInstancesForListItem(ListId, ItemId)  
              .Cast<WorkflowInstance>().First(i => i.Id == workflowID);  
            var wfSubscriptionService = workflowServiceManager.GetWorkflowSubscriptionService();  
            var wfSubscription = wfSubscriptionService.GetSubscription(workflowInstance.WorkflowSubscriptionId);  
            return new Guid(wfSubscription.PropertyDefinitions["TaskListId"]);  
        }  
        public string RenderControl(Control ctrl)  
        {  
            StringBuilder sb = new StringBuilder();  
            StringWriter tw = new StringWriter(sb);  
            HtmlTextWriter hw = new HtmlTextWriter(tw);  
            ctrl.RenderControl(hw);  
            return sb.ToString();  
        }  
   </script>  
And mark old  ListViewByQuery (ID="idTasksView") with Visible="false".

Then add newListViewByQuery near old view:
   <%   
      if (List != null && ListItem != null)  
      {  
           ListViewByQuery idTasksView2 = new ListViewByQuery();  
           string strGuidWorkflow = base.Request.QueryString["WorkflowInstanceName"];  
           Guid wfID = new Guid(strGuidWorkflow);  
           SPWeb web = SPContext.Current.Web;  
           Guid TaskListId = GetTaskListId(web, List.ID, ListItem.ID, wfID);  
           idTasksView2.List = web.Lists.GetList(TaskListId, false);
           SPQuery query = new SPQuery(idTasksView2.List.DefaultView)  
           {  
               RowLimit = Math.Min(idTasksView2.List.DefaultView.RowLimit, base.Site.WebApplication.MaxItemsPerThrottledOperation)  
           };  
           query.Query = "<Where><Eq><FieldRef Name=\"WFInstanceID\"></FieldRef><Value Type=\"Guid\">" + strGuidWorkflow + "</Value></Eq></Where><OrderBy Override='TRUE'><FieldRef Name='ID' /></OrderBy>";  
           query.ViewFields = "<FieldRef Name=\"AssignedTo\" /><FieldRef Name=\"LinkTitle2\" /><FieldRef Name=\"Status\" /><FieldRef Name=\"WorkflowLink\" /><FieldRef Name=\"CustomTaskOutcome\" />";  
           idTasksView2.Query = query;  
           Page.Response.Write(RenderControl(idTasksView2));   
      }       
   %>  
Here in query you see that we filter by WFInstanceID field. This is field that not exists yet.
We need to create it. But first we need to set unhidden to "WF4InstanceId" - built-in workflow instance ID field. Yes, because hidden fields are not being filtered by CAML. Damn!

Also, replace CustomTaskOutcome above in ViewFields to your custom task outcome field name.

Well, now we've finished to edit WrkStat.aspx file and start to work with fields.

Run this once to make WF4InstanceId field unhidden (in console app for example, or you can run PowerShell script below; taskList is your Task List SPList):
    private static void SetCanToggleHidden(SPField field, bool can)
    {
        Type type = field.GetType();
        MethodInfo mi = type.GetMethod("SetFieldBoolValue", BindingFlags.NonPublic | BindingFlags.Instance);
        mi.Invoke(field, new object[] { "CanToggleHidden", can });
    }
    ...
    Guid fieldID = new Guid("1f30d200-0d4e-4c8a-a7eb-2e49815bf2be"); // WF4InstanceId field
    SPField field = taskList.Fields[fieldID];
    SetCanToggleHidden(field, true);
    field.Hidden = false;
    field.Update();
PowerShell version:
 Add-PSSnapin Microsoft.Sharepoint.Powershell -ErrorAction SilentlyContinue
 $web = Get-SPWeb <your_web>
 $list = $web.Lists["<task_list_name>"]
 $field = $list.Fields.GetFieldByInternalName("WF4InstanceId")

 $mi = $field.GetType().GetMethod("SetFieldBoolValue",
   [System.Reflection.BindingFlags]$([System.Reflection.BindingFlags]::NonPublic -bor
   [System.Reflection.BindingFlags]::Instance))
 $mi.Invoke($field, @("CanToggleHidden",$true))
 $field.Hidden=$false
 $field.Update()

Then go to your task list settings and add a Calculated field "WFInstanceID" with formula
=[Instance Id]
Formula may depend on your language settings. For example, russian version:

 
So, now we have field that we need for filtering:


You may also make WF4InstanceId field hidden again if you want.

Well, now open your new WrkStat.aspx page and see your custom field:


That's all, folks!

1 comment:

  1. Здравствуйте Владимир,

    приятно читать ваш блог учитывая как мало в сети полезной информации по работе с потоками в Sharepoint. Если у вас есть хотя бы крупица свободного времени мне бы хотелось проконсультироваться с вами по поводу ошибки которая возникает при релизе потоков в продукцию. Если вкратце то при попытке запуска потока каким бы то ни было образом выдаёт следующее

    An unhandled exception occurred during the execution of the workflow instance. Exception details: System.FormatException: Expected hex 0x in '{0}'. at System.Guid.GuidResult.SetFailure(ParseFailureKind failure, String failureMessageID, Object failureMessageFormatArgument, String failureArgumentName, Exception innerException)

    С виду кажется что не работает парсинг имени листа в guid. Действительно, во всех потоках испольуются подобные выражения:
    System.Guid.Parse("{$ListId:Lists/DamaWorkflowTaskList;}"), но если бы дело было в этом то не работали бы все потоки (больше 10), а ломаются они случайным образом после релизов в продукцию. Проблема решается удалением папки с потоком с сайта и реактивацией feature которая заново генерирует xml потока, но данная процедура не является оптимальной ибо таким образом закрываются существующие потоки. Хотелось бы понять в чём дело дабы исправить причину ошибки. Если у вас будет желание / время помочь с этим, то пожалуйста свяжитесь со мной: babrou93@gmail.com
    Всего вам хорошего!

    ReplyDelete