I have working code (thank you John Socha-Leialoha) that uses the TFS API to retrieve work items, along with all their linked work items (code below). However, what I'm trying to do is access the names of the linked Files (TFS calls it a "Versioned Item") for each work item. In the TFS GUI you can link a file to a work item. Say Work Item 1234 is linked to file foo.txt. Now when I run this query to find linked items, that file is not in the list - only other children WIs or parent WIs are returned. It's the same result if I create and run the query entirely in the GUI. How can I find out which files are linked to a given WI? The only way I can now is to look at the WI in the TFS GUI, and it shows in the Files list in the lower right.
Perhaps I just need to do a normal flat query, fetch the WI fields, and somehow the names of the linked files would be one of the fields of that WI? I don't need to download the linked file, I just need the filename/location.
Code to return all linked WIs is here:
public List<string> GetLinkedItems()
{
//executes a linked item query, returning work items, as well as the items that are link to them.
//gets digital asset work item that contains the given part number in the Assoc. Parts field
var result = new List<string>();
var tpc = new TfsTeamProjectCollection(new Uri(_tfsUri));
var workItemStore = (WorkItemStore) tpc.GetService(typeof (WorkItemStore));
//and [Schilling.TFS.TechPub.AssocParts] CONTAINS '101-4108'
var query =
"SELECT [System.Id], [System.Links.LinkType], [System.TeamProject]," +
" [System.WorkItemType], [System.Title], [System.AssignedTo]," +
" [System.State] FROM WorkItemLinks " +
" WHERE ([Source].[System.TeamProject] = 'Tech Pubs' AND " +
" [Source].[System.WorkItemType] = 'DigitalAsset' AND " +
" [Source].[System.State] <> '') And " +
" ([System.Links.LinkType] <> '') And " +
" ([Target].[System.WorkItemType] <> '') " +
" ORDER BY [System.Id] mode(MayContain)";
var treeQuery = new Query(workItemStore, query);
//Note we need to call RunLinkQuery here, not RunQuery, because we are doing a link item type of query
var links = treeQuery.RunLinkQuery();
//// Build the list of work items for which we want to retrieve more information//
int[] ids = (from WorkItemLinkInfo info in links
select info.TargetId).Distinct().ToArray();
//
// Next we want to create a new query that will retrieve all the column values from the original query, for
// each of the work item IDs returned by the original query.
//
var detailsWiql = new StringBuilder();
detailsWiql.AppendLine("SELECT");
bool first = true;
foreach (FieldDefinition field in treeQuery.DisplayFieldList)
{
detailsWiql.Append(" ");
if (!first)
detailsWiql.Append(",");
detailsWiql.AppendLine("[" + field.ReferenceName + "]");
first = false;
}
detailsWiql.AppendLine("FROM WorkItems");
//
// Get the work item details
//
var flatQuery = new Query(workItemStore, detailsWiql.ToString(), ids);
WorkItemCollection details = flatQuery.RunQuery();
return
(from WorkItem wi in details
select wi.Id + ", " + wi.Project.Name + ", " + wi.Title + ", " + wi.State).ToList();
}
Work item queries can only show WorkItemLinkType links. To get links of other types (i.e. files in source control), you need to go through the list of links in the WorkItem object itself. Here's a snippet of code that will get you the artifact of the file linked to the work item:
TfsTeamProjectCollection tpc = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(
new Uri("http://<server>:8080/tfs/<collection>"));
WorkItemStore wiStore = tpc.GetService<WorkItemStore>();
VersionControlServer vc = tpc.GetService<VersionControlServer>();
WorkItem task = wiStore.GetWorkItem(<work item with a linked file>);
var externalLinks = task.Links.OfType<ExternalLink>();
foreach (var link in externalLinks)
{
XmlDocument artifact = vc.ArtifactProvider.GetArtifactDocument(new Uri(link.LinkedArtifactUri));
}
The XML document contains all necessary information needed to grab the correct file version from the VersionControlServer using the GetItem() method.
Related
I'm trying to piece together a C# console app that accesses the work items in TFS/DevOps via it's API and compares the original estimate field parent work item with that of all its children and then spits out the name and ID of any work items that do not add up.
So far, I have been able to get back a list of all my work items with the original estimates included, but I still need to get the children of each work item so that I can loop through them and compare the summation of their original estimates with that of their parent. Given how little I know about C# and queries I am pretty stuck right now.
Since linked items are not reportable fields, I have to use $expand to execute a query to get the info I need (at least that's what the doc linked below says). This is where I am stuck. Any tips?
https://learn.microsoft.com/en-us/azure/devops/report/extend-analytics/work-item-links?view=azure-devops
Here is what I have so far.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
using Microsoft.VisualStudio.Services.Common;
namespace QueryWorkitems0619
{
class Program
{
static void Main(string[] args)
{
string orgName = "{Organization's name}";
string PAT = "{Personal Access Token}";
Uri uri = new Uri($"https://dev.azure.com/{orgName}");
string project = "Wingnit_2";
VssBasicCredential credentials = new VssBasicCredential("", PAT);
//create a wiql object and build our query
Wiql wiql = new Wiql()
{
Query = "Select * " +
"From WorkItems " +
"Where [System.TeamProject] = '" + project + "' " +
"And [System.State] <> 'Closed' " +
"And [System.RelatedLinkCount] > '0'" +
"Order By [State] Asc, [Changed Date] Desc"
};
//create instance of work item tracking http client
using (WorkItemTrackingHttpClient workItemTrackingHttpClient = new WorkItemTrackingHttpClient(uri, credentials))
{
//execute the query to get the list of work items in the results
WorkItemQueryResult workItemQueryResult = workItemTrackingHttpClient.QueryByWiqlAsync(wiql).Result;
//some error handling
if (workItemQueryResult.WorkItems.Count() != 0)
{
//need to get the list of our work item ids and put them into an array
List<int> list = new List<int>();
foreach (var item in workItemQueryResult.WorkItems)
{
list.Add(item.Id);
}
int[] arr = list.ToArray();
//build a list of the fields we want to see
string[] fields = new string[5];
fields[0] = "System.Id";
fields[1] = "System.Title";
fields[2] = "System.RelatedLinkCount";
fields[3] = "System.Description";
fields[4] = "Microsoft.VSTS.Scheduling.OriginalEstimate";
//get work items for the ids found in query
var workItems = workItemTrackingHttpClient.GetWorkItemsAsync(arr, fields, workItemQueryResult.AsOf).Result;
//loop though work items and write to console
foreach (var workItem in workItems)
{
foreach (var field in workItem.Fields)
{
Console.WriteLine("- {0}: {1}", field.Key, field.Value);
}
}
Console.ReadLine();
}
}
}
}
}
You are in the right direction, you need to add the expand when you execute the GetWorkItemsAsync method:
var workItems = workItemTrackingHttpClient.GetWorkItemsAsync(arr, expand: WorkItemExpand.Relations workItemQueryResult.AsOf).Result;
Note: you can't use fields with expand together, so you need to remove it (you will get all the fields).
Loop the results, inside the work item result you will see Relations, check inside the Relations if the Rel is System.LinkTypes.Hierarchy-Forward, if yes - it's a child:
Now, you have the child id inside the Url, extract it and make an API to get his details.
I create a site collection column that itself has a lookup onto a list inside that site collection. I create this column using CSOM:
string contextUrl = "http://company.example.com/sites/mysite/subsite";
SharePointContext spContext = SharePointContextProvider.Current.GetSharePointContext(HttpContext.Current);
ClientContext clientContext = new ClientContext(contextUrl);
Web web = clientContext.Web;
clientContext.Load(web);
clientContext.ExecuteQuery();
Web rootWeb = clientContext.Site.RootWeb;
clientContext.Load(rootWeb);
// Add List containing target column
ListCreationInformation targetListInfo = new ListCreationInformation();
targetListInfo.Title = "TargetListTitle";
targetListInfo.TemplateType = (int)ListTemplateType.GenericList;
List targetList = web.Lists.Add(targetListInfo);
targetList.Update();
clientContext.Load(targetList);
clientContext.ExecuteQuery();
// Update Title
FieldCollection techListFields = targetList.Fields;
clientContext.Load(techListFields);
clientContext.ExecuteQuery();
// Create Site Lookupcolumns
var techListID = targetList.Id;
FieldCollection colSCFields = rootWeb.Fields;
clientContext.Load(colSCFields);
clientContext.ExecuteQuery();
var lookupSchema = "<Field Type='Lookup' DisplayName='Magic' Required='FALSE' List='" + techListID + "' ShowField='Title' StaticName='Magic' Name='Magic'/>";
colSCFields.AddFieldAsXml(lookupSchema, false, AddFieldOptions.AddFieldInternalNameHint);
clientContext.ExecuteQuery();
(for complete information I added all lines, but the crucial part start below "// Create Site Lookupcolumns")
The site column is being created, but when I use a lookup onto that column on a list inside a sub site (manually or by program) the dropdown from that lookup field does not display any content. (The behavior looks like a lookup column being created before the target column exists)
You need to specify WebId attribute value (set it to subsite ID) in the field xml definition, like this:
var lookupSchema = "<Field Type='Lookup' DisplayName='Magic' Required='FALSE' WebId='" + web.Id + "' List='" + techListID + "' ShowField='Title' StaticName='Magic' Name='Magic'/>";
I'm trying to filter a SharePoint list so that only the items with the Management field, which holds a string, as "Yes" will show up, but whenever I get to the ctx.ExecuteQuery() statement, my program blows up. I believe my CAMLQuery is structured correctly, so I'm not sure if I'm simply using it wrong or if I'm missing something. Any help would be great! thanks! The code I currently have is posted below:
Web myWeb = ctx.Web;
List myList = myWeb.Lists.GetByTitle("Company Employees");
SPClient.View view = myList.DefaultView;
CamlQuery qry = new CamlQuery();
qry.ViewXml = "<Query>" + "< Where >" + "<Eq>" + "< FieldRef Name='Management'/>" + "< Value Type='Text'>Yes</ Value >" + "</Eq>" + "</ Where >" + "</ Query >";
myList.GetItems(qry);
ListItemCollection listItems = myList.GetItems(qry);
ctx.Load(listItems);
ctx.ExecuteQuery();
Your code appears to be missing the <View> tag which would wrap around your <Query> tag in the CAML.
With the addition of the <View> root element, the correct CAML XML would be as follows:
qry.ViewXml =
"<View>"+
"<Query>"+
"<Where>"+
"<Eq>"+
"<FieldRef Name='Management'/>"+
"<Value Type='Text'>Yes</Value>"+
"</Eq>"+
"</Where>"+
"</Query>"+
"</View>";
Additional Troubleshooting
To help troubleshoot, you can try running the same query through the JavaScript client object model.
Visit the SharePoint site in Internet Explorer and hit F5 to open up the developer tools.
On the Console tab, enter the following lines of code and execute (by pressing Enter or Ctrl+Enter) them one line at a time:
-
var ctx = new SP.ClientContext();
var list = ctx.get_web().get_lists().getByTitle("Company Employees");
var qry = new SP.CamlQuery();
qry.set_viewXml("<View><Query><Where><Eq><FieldRef Name=\"Management\"/><Value Type=\"Text\">Yes</Value></Eq></Where></Query></View>");
var items = list.getItems(qry);
ctx.load(items);
ctx.executeQueryAsync(function(){alert("success!");},function(sender,args){alert(args.get_message());});
POST HELP SOLUTION Thanks to your help, I was able to figure out how to create a new view with the desired filtering by using the following code. The main problem was with the Caml Query--I had to remove the and tags and then delete a few of the lines before creating the view. Below is my working solution:
Web myWeb = ctx.Web;
List myList = myWeb.Lists.GetByTitle("Company Employees");
SPClient.View view = myList.DefaultView;
CamlQuery qry = new CamlQuery();
qry.ViewXml =
"<Where><Eq><FieldRef Name=\"Management\"/><Value Type='Text'>Yes</Value></Eq></Where>";
ViewCollection viewColl = myList.Views;
string[] viewFields = { "Title", "Promoted", "Intern", "Management" };
ViewCreationInformation creationInfo = new ViewCreationInformation();
creationInfo.Title = "Management";
creationInfo.RowLimit = 50;
creationInfo.ViewFields = viewFields;
creationInfo.ViewTypeKind = ViewType.None;
creationInfo.SetAsDefaultView = false;
creationInfo.Query = qry.ViewXml;
viewColl.Add(creationInfo);
ctx.ExecuteQuery();
I need to query a list in SharePoint where the columns may be added in the future.
For instance at the moment I have the following columns
Name, Job, interests, address
I want to be able to query this string dynamically using a parameter from the browser so if columns are added in the future I don’t have to change the code but just the parameter.
The address may look like this www.contoso.com/sites/mypage.aspx?property=Interests
And the code something on the line of this:
var SiteParameter = Request.QueryString["property"];
var ItemsFromList = from item in ListItems where item[try to put the parameter in here] select item;
I use SPmetal to get the list details, so if I press item. Visual Studio2010 will return the columns within the list.
This may be easier without SPMetal.
var qy = new SPQuery();
qy.Query =
"<Where><Eq>" +
"<FieldRef Name=`" + siteParameter + "'/>" +
// You may have to worry about the type of the field here, too.
"<Value Type='Text'>" + desiredValue + "</Value>" +
"</Eq></Where>";
var itemCollection = myList.GetItems(qy);
Given a certain product backlog id, I want to programmatically retrieve a list of tasks that are child to the PBI.
I am aware that there's not one field in the task that says "Parent PBI Id". I have a version of code that is working, but that's really really slow, since I have do perform part of my filtering int the client.
See how I'm currently doing:
string wiqlQuery =
string.Format(
"Select ID, [Remaining Work], State " +
"from WorkItems " +
"where (([Work Item Type] = 'Task')" +
" AND ([Iteration Path] = '{0}' )" +
" AND (State <> 'Removed')" +
" AND (State <> 'Done')) ",
sprint, storyId);
// execute the query and retrieve a collection of workitems
WorkItemCollection workItems = wiStore.Query(wiqlQuery);
if (workItems.Count <= 0)
return null;
var result = new List<TaskViewModel>();
foreach (WorkItem workItem in workItems)
{
var relatedLink = workItem.Links[0] as RelatedLink;
if (relatedLink == null) continue;
if (relatedLink.RelatedWorkItemId != storyId) continue;
Field remWorkField = (from field in workItem.Fields.Cast<Field>()
where field.Name == "Remaining Work"
select field).FirstOrDefault();
if (remWorkField == null) continue;
if (remWorkField.Value == null) continue;
var task = new TaskViewModel
{
Id = workItem.Id,
ParentPbi = relatedLink.RelatedWorkItemId,
RemainingWork = (double) remWorkField.Value,
State = workItem.State
};
result.Add(task);
}
return result;
As a standard, team project in MSF Agile comes with a set of queries. Take a look at 'Work Items' -> 'Iteration 1' -> 'Iteration Backlog'.
Saving this query as WIQL file in your disk is absolutely possible. Using it as a modified wiqlQuery should relieve you from a lot of the filtering you do.
EDIT (in response to comment: "Ok, I did that, but the query doesn't mention the relationship between parent and linked (child) items"):
I opened the WIQL of a default "Iteration Backlog":
<?xml version="1.0" encoding="utf-8"?>
<WorkItemQuery Version="1">
<TeamFoundationServer>
http://****>
<TeamProject>****</TeamProject>
<Wiql>SELECT [System.Id], [System.WorkItemType], [System.Title],
[System.State], [System.AssignedTo],
[Microsoft.VSTS.Scheduling.RemainingWork],
[Microsoft.VSTS.Scheduling.CompletedWork],
[Microsoft.VSTS.Scheduling.StoryPoints],
[Microsoft.VSTS.Common.StackRank],
[Microsoft.VSTS.Common.Priority],
[Microsoft.VSTS.Common.Activity], [System.IterationPath],
[System.AreaPath] FROM WorkItemLinks WHERE
(Source.[System.TeamProject] = #project and
Source.[System.AreaPath] under #project and
Source.[System.IterationPath] under '****\Iteration 1' and
(Source.[System.WorkItemType] = 'User Story' or
Source.[System.WorkItemType] = 'Task')) and
[System.Links.LinkType] = 'System.LinkTypes.Hierarchy-Forward'
and Target.[System.WorkItemType] = 'Task' ORDER BY
[Microsoft.VSTS.Common.StackRank],
[Microsoft.VSTS.Common.Priority] mode(Recursive)</Wiql>
</WorkItemQuery>
The part of the query that retrieves the related items should be this
[System.Links.LinkType] = 'System.LinkTypes.Hierarchy-Forward'