none
How do I get filenames of checked out files from PendChangesNotification?

    Question

  • How do I get information about the pending changes when a user checks out a file from TFS 2010?  I need to know what files the user is trying to checkout.
     
    I'm writing a TFS server-side subscriber plugin that subscribes to PendChangesNotification.
     
    I've implemented my plugin following the information in Martin Hinshelwood's blog post Team Foundation Server 2010 Event Handling with Subscribers.
     
    I was hoping that PendingChanges property of the PendChangesNotification class would lead me to the information about which file was being checked out, but in my tests PendingChanges.Length is always zero.
     
    The documentation online for the Microsoft.TeamFoundation.VersionControl.Server.PendChangesNotification class does not provide any information about the class members other than their name and type.

    Interestingly if I subscribe to UndoPendingChangesNotification the ItemsToUndo property contains a list file paths within version control (for example, "$/TestProj/TestApp1/Source/Class1.cs") when the NotificationType is DecisionPoint, but not when the NotificationType is Notification.  It seems strange to me that I can get the file path information for UndoPendingChangesNotification when I can't get it for PendChangesNotification.
     
    Any help with this is greatly appreciated.


    Bryan Knox
    Monday, February 28, 2011 6:10 PM

Answers

  • You might find this interesting for info (in VB)

                ' First Get the local file path
                Dim params1 As NameValueCollection = requestContext.Method.Parameters
                Dim workspacename As String = params1("workspaceName")
                Dim requesttype As String = params1("changes[0].RequestType")
                Dim localfilepath As String = params1("changes[0].ItemSpec")
     
                ' Debug Output
                For Each key In params1.AllKeys
                    Dim keyval As String = key
                    Dim val As String = params1(key)
                    Debug.WriteLine("key:" & keyval & " val:" & val)
                Next
     
                ' Next get the Workspace
                Dim vc As TeamFoundationVersionControlService = requestContext.GetService(Of TeamFoundationVersionControlService)()
                Dim tmpws As Workspace = vc.QueryWorkspace(requestContext, workspacename, notificationEventArgs.UserName)
     
                ' Next get the SourceControl Path
                Dim serverpath As String = ""
                For Each wf As WorkingFolder In tmpws.Folders
                    If wf.Type = WorkingFolderType.Map And localfilepath.Contains(wf.LocalItem) Then
                        Dim tmp_str As String = localfilepath.Replace(wf.LocalItem, "")
                        tmp_str = tmp_str.Replace("\""/")
                        serverpath = wf.ServerItem & tmp_str
                        Exit For
                    End If
                Next
                Debug.WriteLine("Serverpath: " & serverpath)
    • Marked as answer by BryanKnox Tuesday, November 01, 2011 5:14 PM
    Tuesday, November 01, 2011 9:22 AM
  • The below code might be more useful, I've discovered that ItemSpec sometimes also contains the recursion type at the end if you check out a folder instead of a file

    also with folders it looks like it's the server path of $/ form instead of the local path

    What I'm trying to figure out next, is if it's possible to re-use the requestcontext to do another checkout on the back of the first (I'm trying to write a plugin for Shared Files similar to the old sourcesafe)

     

    Example call

                    ' Get the Version Control Service
                    VCService = RequestContext.GetService(Of TeamFoundationVersionControlService)()
                    ' Get the Workspace Name
                    WorkSpaceName = NotificationEventArgs.WorkspaceName
                    ' Get the Workspace instance
                    RequestWorkspace = VCService.QueryWorkspace(RequestContext, WorkSpaceName, NotificationEventArgs.UserName)
                    ' Get a List of Files / directories ready to be checked out
                    ChangeRequests = VCChangeRequest.NVC_To_ChangeRequestList(RequestContext.Method.Parameters, RequestWorkspace)

    VCChangeRequest.vb

    Imports Microsoft.TeamFoundation.VersionControl.Server
    Imports System.Collections.Specialized
     
    Namespace Containers
     
        ''' <summary>
        ''' Custom ChangeRequest class, inherits from TFs Server ChangeRequest
        ''' </summary>
        Public Class VCChangeRequest
            Inherits ChangeRequest
     
    #Region "Public Properties"
     
            ''' <summary>
            ''' Server Request Parameters that were used to construct this Change Request
            ''' </summary>
            Public Property ServerRequestParameters As NameValueCollection
     
            ''' <summary>
            ''' ID of the ChangeRequest within the Server Request Parameters
            ''' </summary>
            Public Property ID As Integer
     
            ''' <summary>
            ''' Local File Path to the file
            ''' </summary>
            Public ReadOnly Property LocalPath As String
                Get
                    Return Me.ItemSpec.Item
                End Get
            End Property
     
            ''' <summary>
            ''' Server Path to the file (e.g. $/ form)
            ''' </summary>
            Public Property ServerPath As String
     
    #End Region
     
    #Region "Public Constructors"
     
            ''' <summary>
            ''' Default Constructor
            ''' </summary>
            Public Sub New()
            End Sub
     
            ''' <summary>
            ''' Default Constructor
            ''' </summary>
            Public Sub New(nvc As NameValueCollection, index As Integer)
                Me.ServerRequestParameters = nvc
                Me.ID = index
                Dim keyheader As String = "changes[" & index & "]"
                If nvc.AllKeys.Contains(keyheader & ".DeletionId"Then
                    Dim val As String = nvc(keyheader & ".DeletionId")
                    Integer.TryParse(val, Me.DeletionId)
                End If
                If nvc.AllKeys.Contains(keyheader & ".Encoding"Then
                    Dim val As String = nvc(keyheader & ".Encoding")
                    Integer.TryParse(val, Me.Encoding)
                End If
                If nvc.AllKeys.Contains(keyheader & ".ItemSpec"Then
                    Dim val As String = nvc(keyheader & ".ItemSpec")
     
                    ' Determine the item name
                    ' If the RecursionType has been specified then remove this from the end of the string
                    Dim itemname As String = val
                    For Each name As String In [Enum].GetNames(GetType(RecursionType))
                        Dim tmp_str As String = " (" & name & ")"
                        If itemname.EndsWith(tmp_str) Then itemname = itemname.Substring(0, itemname.LastIndexOf(tmp_str))
                    Next
     
                    ' Determine the recursion type
                    Dim resurstype As RecursionType = RecursionType.None
                    For Each name As String In [Enum].GetNames(GetType(RecursionType))
                        Dim tmp_str As String = " (" & name & ")"
                        If val.EndsWith(tmp_str) Then [Enum].TryParse(name, resurstype)
                    Next
     
                    Me.ItemSpec = New ItemSpec(itemname, resurstype)
                End If
                If nvc.AllKeys.Contains(keyheader & ".LockLevel"Then
                    Dim val As String = nvc(keyheader & ".LockLevel")
                    [Enum].TryParse(val, Me.LockLevel)
                End If
                If nvc.AllKeys.Contains(keyheader & ".RequestType"Then
                    Dim val As String = nvc(keyheader & ".RequestType")
                    [Enum].TryParse(val, Me.RequestType)
                End If
                If nvc.AllKeys.Contains(keyheader & ".TargetItem"Then
                    Dim val As String = nvc(keyheader & ".TargetItem")
                    Me.TargetItem = val
                End If
                If nvc.AllKeys.Contains(keyheader & ".TargetItemType"Then
                    Dim val As String = nvc(keyheader & ".TargetItemType")
                    [Enum].TryParse(val, Me.TargetItemType)
                End If
            End Sub
     
    #End Region
     
    #Region "Public Functions"
     
            ''' <summary>
            ''' Calculate the Server Path based on the work space / local path
            ''' </summary>
            Public Sub DetermineServerPath(ws As Workspace)
                If LocalPath.StartsWith("$/"Then
                    ServerPath = LocalPath
                    Exit Sub
                End If
                For Each wf As WorkingFolder In ws.Folders
                    If wf.Type = WorkingFolderType.Map And Me.LocalPath.Contains(wf.LocalItem) Then
                        Dim tmp_str As String = Me.LocalPath.Replace(wf.LocalItem, "")
                        tmp_str = tmp_str.Replace("\""/")
                        ServerPath = wf.ServerItem & tmp_str
                        Exit For
                    End If
                Next
            End Sub
     
    #End Region
     
    #Region "Public Shared Functions"
     
            ''' <summary>
            ''' Convert a NameValueCollection / List of Parameters to a List of VCChangeRequest
            ''' </summary>
            ''' <remarks>Typically Requests come in to the server as a NameValueCollection
            ''' This Function Converts them to a List of ChangeRequests</remarks>
            Public Shared Function NVC_To_ChangeRequestList(nvc As NameValueCollectionAs List(Of ChangeRequest)
                Return NVC_To_ChangeRequestList(nvc, Nothing)
            End Function
     
            ''' <summary>
            ''' Convert a NameValueCollection / List of Parameters to a List of VCChangeRequest
            ''' </summary>
            ''' <remarks>Typically Requests come in to the server as a NameValueCollection
            ''' This Function Converts them to a List of ChangeRequests</remarks>
            Public Shared Function NVC_To_ChangeRequestList(nvc As NameValueCollection, ws As WorkspaceAs List(Of ChangeRequest)
                Dim retval As New List(Of ChangeRequest)
                Dim CRCount As Integer
                If nvc.AllKeys.Contains("changes") = False Then Return Nothing
                Dim tmp_count As String = nvc("changes")
                tmp_count = tmp_count.Replace("Count =""").Trim(" ")
                If Integer.TryParse(tmp_count, CRCount) = False Then Return Nothing
                For i As Integer = 0 To (CRCount - 1)
                    Dim newCR As New VCChangeRequest(nvc, i)
                    If ws IsNot Nothing Then newCR.DetermineServerPath(ws)
                    retval.Add(newCR)
                Next
                Return retval
            End Function
     
    #End Region
     
        End Class
     
    End Namespace
    • Marked as answer by BryanKnox Wednesday, November 02, 2011 4:32 PM
    Wednesday, November 02, 2011 4:18 PM

All replies

  • Hello Bryan,

    Thanks for your post and sorry for the late reply.

    It is a shame that I am not familiar with PendChangesNotification class. However, one method I can think out to get the filename of these checked out files is that you can use the tf.exe status command line.

    You can have your command line to be similar to:

    tf.exe status /collection:TeamProjectCollectionURL /user:* /recursive > c:\checkedout.txt

    “c:\checkedout.txt” is the path to save the output.

    You can refer to this article for further information about the Status Command.

    http://msdn.microsoft.com/en-us/library/9s5ae285.aspx

    In addition, you can also take a look at this blog:

    http://blogs.msdn.com/b/technical_outburst/archive/2008/06/09/how-to-get-the-list-of-currently-checked-out-files-in-tfs.aspx

    I hope it can help you with your question.

    Thanks,


    Vicky Song [MSFT]
    MSDN Community Support | Feedback to us
    Get or Request Code Sample from Microsoft
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.

    Wednesday, March 02, 2011 3:06 AM
  • Hi Vicky,

    Thanks for the tips.  Unfortunately my plug-in needs to run on the TFS server where I told installing VS and other client tools is not recommended.

    I guess I could figure out how to get the information I need directly from the SQL database when my plugin receives a PendChangesNotification, but I'd really rather use the APIs and not break encapsulation.

    Is there anywhere else I can go to learn more about using PendChangesNotification and other classes in the
    Microsoft.TeamFoundation.VersionControl.Server namespace?

     


    Bryan Knox
    Wednesday, March 02, 2011 5:52 PM
  • Hello Bryan,

    You can refer to these articles for further information about PendChangesNotification class:

    PendChangesNotification Methods

    http://msdn.microsoft.com/en-us/library/microsoft.teamfoundation.versioncontrol.server.pendchangesnotification_methods.aspx

    PendChangesNotification Properties

    http://msdn.microsoft.com/en-us/library/microsoft.teamfoundation.versioncontrol.server.pendchangesnotification_properties.aspx

    And more classes information in the namespace Microsoft.TeamFoundation.VersionControl.Server:

    http://msdn.microsoft.com/en-us/library/microsoft.teamfoundation.versioncontrol.server.aspx

    I hope it can help you with your issue.

    Thanks,


    Vicky Song [MSFT]
    MSDN Community Support | Feedback to us
    Get or Request Code Sample from Microsoft
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.

    Friday, March 04, 2011 9:13 AM
  • Hi Vicky,

    I had arleady seen those articles, they only contain member name and type information and no other documentation.  No help.

    Thanks.

     


    Bryan Knox
    Saturday, March 05, 2011 4:05 AM
  • Hello Bryan,

    I am sorry the links above do not help you a lot. I have escalated your issue to my mentor, and he may help you write the sample code. Once he finished it, I will share it with you.

    Thanks,


    Vicky Song [MSFT]
    MSDN Community Support | Feedback to us
    Get or Request Code Sample from Microsoft
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.

    Wednesday, March 09, 2011 3:27 AM
  • I experience the very same issue :(

    I want to validate the creation of a Branch during a Notification's DecisionPoint, and before the check-in of that new branch. The only event I can subscribe, to be notified of this creation before the check-in, is therefore PendChangesNotification. But it does not contain any useful information :(

    (I get a PendChangesNotification only when the creation convert the source folder into a branch. If it's already a branch, I get directly a check-in notification).

    Help would really be welcome on this topic!

     

    Notice I did try to fetch all the pendingChanges (using the code here after) but didn't get the newly created Branch in the output reader, meaning that it's not yet (?) a "pendingChange" during the DecisionPoint. However, the name of target branch is already known at least by Visual Studio and should be made available for TFS! So, where is this info ???

    var versionControl = requestContext.GetService<TeamFoundationVersionControlService>();
    PendChangesNotification arg = notificationEventArgs as PendChangesNotification;
    if (arg != null)
    {
    ItemSpec item = new ItemSpec("$"RecursionType.Full);
    var itemSpecs = new ItemSpec[] { item };
    var output = tempVersionControl.QueryPendingChangesForWorkspace(
                        requestContext, arg.WorkspaceName, arg.UserName,
                        itemSpecs, true, 9999, nulltrue);  

     

    Did you find anything helpful Bryan ?

    V.


    Tuesday, October 04, 2011 9:42 AM
  • Hi Valéry,

    I haven't found anything helpful yet.  I'll post an update here if I do.

    Kind regards.


    Bryan Knox
    Tuesday, October 04, 2011 5:24 PM
  • You might find this interesting for info (in VB)

                ' First Get the local file path
                Dim params1 As NameValueCollection = requestContext.Method.Parameters
                Dim workspacename As String = params1("workspaceName")
                Dim requesttype As String = params1("changes[0].RequestType")
                Dim localfilepath As String = params1("changes[0].ItemSpec")
     
                ' Debug Output
                For Each key In params1.AllKeys
                    Dim keyval As String = key
                    Dim val As String = params1(key)
                    Debug.WriteLine("key:" & keyval & " val:" & val)
                Next
     
                ' Next get the Workspace
                Dim vc As TeamFoundationVersionControlService = requestContext.GetService(Of TeamFoundationVersionControlService)()
                Dim tmpws As Workspace = vc.QueryWorkspace(requestContext, workspacename, notificationEventArgs.UserName)
     
                ' Next get the SourceControl Path
                Dim serverpath As String = ""
                For Each wf As WorkingFolder In tmpws.Folders
                    If wf.Type = WorkingFolderType.Map And localfilepath.Contains(wf.LocalItem) Then
                        Dim tmp_str As String = localfilepath.Replace(wf.LocalItem, "")
                        tmp_str = tmp_str.Replace("\""/")
                        serverpath = wf.ServerItem & tmp_str
                        Exit For
                    End If
                Next
                Debug.WriteLine("Serverpath: " & serverpath)
    • Marked as answer by BryanKnox Tuesday, November 01, 2011 5:14 PM
    Tuesday, November 01, 2011 9:22 AM
  • Thanks for the code Richard!  That gets me what I need.


    Bryan Knox
    Tuesday, November 01, 2011 5:17 PM
  • The below code might be more useful, I've discovered that ItemSpec sometimes also contains the recursion type at the end if you check out a folder instead of a file

    also with folders it looks like it's the server path of $/ form instead of the local path

    What I'm trying to figure out next, is if it's possible to re-use the requestcontext to do another checkout on the back of the first (I'm trying to write a plugin for Shared Files similar to the old sourcesafe)

     

    Example call

                    ' Get the Version Control Service
                    VCService = RequestContext.GetService(Of TeamFoundationVersionControlService)()
                    ' Get the Workspace Name
                    WorkSpaceName = NotificationEventArgs.WorkspaceName
                    ' Get the Workspace instance
                    RequestWorkspace = VCService.QueryWorkspace(RequestContext, WorkSpaceName, NotificationEventArgs.UserName)
                    ' Get a List of Files / directories ready to be checked out
                    ChangeRequests = VCChangeRequest.NVC_To_ChangeRequestList(RequestContext.Method.Parameters, RequestWorkspace)

    VCChangeRequest.vb

    Imports Microsoft.TeamFoundation.VersionControl.Server
    Imports System.Collections.Specialized
     
    Namespace Containers
     
        ''' <summary>
        ''' Custom ChangeRequest class, inherits from TFs Server ChangeRequest
        ''' </summary>
        Public Class VCChangeRequest
            Inherits ChangeRequest
     
    #Region "Public Properties"
     
            ''' <summary>
            ''' Server Request Parameters that were used to construct this Change Request
            ''' </summary>
            Public Property ServerRequestParameters As NameValueCollection
     
            ''' <summary>
            ''' ID of the ChangeRequest within the Server Request Parameters
            ''' </summary>
            Public Property ID As Integer
     
            ''' <summary>
            ''' Local File Path to the file
            ''' </summary>
            Public ReadOnly Property LocalPath As String
                Get
                    Return Me.ItemSpec.Item
                End Get
            End Property
     
            ''' <summary>
            ''' Server Path to the file (e.g. $/ form)
            ''' </summary>
            Public Property ServerPath As String
     
    #End Region
     
    #Region "Public Constructors"
     
            ''' <summary>
            ''' Default Constructor
            ''' </summary>
            Public Sub New()
            End Sub
     
            ''' <summary>
            ''' Default Constructor
            ''' </summary>
            Public Sub New(nvc As NameValueCollection, index As Integer)
                Me.ServerRequestParameters = nvc
                Me.ID = index
                Dim keyheader As String = "changes[" & index & "]"
                If nvc.AllKeys.Contains(keyheader & ".DeletionId"Then
                    Dim val As String = nvc(keyheader & ".DeletionId")
                    Integer.TryParse(val, Me.DeletionId)
                End If
                If nvc.AllKeys.Contains(keyheader & ".Encoding"Then
                    Dim val As String = nvc(keyheader & ".Encoding")
                    Integer.TryParse(val, Me.Encoding)
                End If
                If nvc.AllKeys.Contains(keyheader & ".ItemSpec"Then
                    Dim val As String = nvc(keyheader & ".ItemSpec")
     
                    ' Determine the item name
                    ' If the RecursionType has been specified then remove this from the end of the string
                    Dim itemname As String = val
                    For Each name As String In [Enum].GetNames(GetType(RecursionType))
                        Dim tmp_str As String = " (" & name & ")"
                        If itemname.EndsWith(tmp_str) Then itemname = itemname.Substring(0, itemname.LastIndexOf(tmp_str))
                    Next
     
                    ' Determine the recursion type
                    Dim resurstype As RecursionType = RecursionType.None
                    For Each name As String In [Enum].GetNames(GetType(RecursionType))
                        Dim tmp_str As String = " (" & name & ")"
                        If val.EndsWith(tmp_str) Then [Enum].TryParse(name, resurstype)
                    Next
     
                    Me.ItemSpec = New ItemSpec(itemname, resurstype)
                End If
                If nvc.AllKeys.Contains(keyheader & ".LockLevel"Then
                    Dim val As String = nvc(keyheader & ".LockLevel")
                    [Enum].TryParse(val, Me.LockLevel)
                End If
                If nvc.AllKeys.Contains(keyheader & ".RequestType"Then
                    Dim val As String = nvc(keyheader & ".RequestType")
                    [Enum].TryParse(val, Me.RequestType)
                End If
                If nvc.AllKeys.Contains(keyheader & ".TargetItem"Then
                    Dim val As String = nvc(keyheader & ".TargetItem")
                    Me.TargetItem = val
                End If
                If nvc.AllKeys.Contains(keyheader & ".TargetItemType"Then
                    Dim val As String = nvc(keyheader & ".TargetItemType")
                    [Enum].TryParse(val, Me.TargetItemType)
                End If
            End Sub
     
    #End Region
     
    #Region "Public Functions"
     
            ''' <summary>
            ''' Calculate the Server Path based on the work space / local path
            ''' </summary>
            Public Sub DetermineServerPath(ws As Workspace)
                If LocalPath.StartsWith("$/"Then
                    ServerPath = LocalPath
                    Exit Sub
                End If
                For Each wf As WorkingFolder In ws.Folders
                    If wf.Type = WorkingFolderType.Map And Me.LocalPath.Contains(wf.LocalItem) Then
                        Dim tmp_str As String = Me.LocalPath.Replace(wf.LocalItem, "")
                        tmp_str = tmp_str.Replace("\""/")
                        ServerPath = wf.ServerItem & tmp_str
                        Exit For
                    End If
                Next
            End Sub
     
    #End Region
     
    #Region "Public Shared Functions"
     
            ''' <summary>
            ''' Convert a NameValueCollection / List of Parameters to a List of VCChangeRequest
            ''' </summary>
            ''' <remarks>Typically Requests come in to the server as a NameValueCollection
            ''' This Function Converts them to a List of ChangeRequests</remarks>
            Public Shared Function NVC_To_ChangeRequestList(nvc As NameValueCollectionAs List(Of ChangeRequest)
                Return NVC_To_ChangeRequestList(nvc, Nothing)
            End Function
     
            ''' <summary>
            ''' Convert a NameValueCollection / List of Parameters to a List of VCChangeRequest
            ''' </summary>
            ''' <remarks>Typically Requests come in to the server as a NameValueCollection
            ''' This Function Converts them to a List of ChangeRequests</remarks>
            Public Shared Function NVC_To_ChangeRequestList(nvc As NameValueCollection, ws As WorkspaceAs List(Of ChangeRequest)
                Dim retval As New List(Of ChangeRequest)
                Dim CRCount As Integer
                If nvc.AllKeys.Contains("changes") = False Then Return Nothing
                Dim tmp_count As String = nvc("changes")
                tmp_count = tmp_count.Replace("Count =""").Trim(" ")
                If Integer.TryParse(tmp_count, CRCount) = False Then Return Nothing
                For i As Integer = 0 To (CRCount - 1)
                    Dim newCR As New VCChangeRequest(nvc, i)
                    If ws IsNot Nothing Then newCR.DetermineServerPath(ws)
                    retval.Add(newCR)
                Next
                Return retval
            End Function
     
    #End Region
     
        End Class
     
    End Namespace
    • Marked as answer by BryanKnox Wednesday, November 02, 2011 4:32 PM
    Wednesday, November 02, 2011 4:18 PM
  • It looks like requestContext.Method.Parameters contains maximum 10 changes - there is never "changes[10].ItemSpec". If you do checkout more then 10 files at the same time you will miss some :(.
    Wednesday, November 02, 2011 5:13 PM
  • I'll have a look at that next, using VCService.CheckPendingChanges with a new requestcontext might be one way around it

    in the mean time, I've discovered that if you want to create a new RequestContext (for example to check out a 2nd file when the 1st file is checked out during the 1st event), you can use

    newcontext = RequestContext.ServiceHost.CreateSystemContext()
    

    e.g.

                    ' Create a change request
                    Dim newchange As New ChangeRequest
                    newchange.RequestType = RequestType.Edit
                    newchange.ItemSpec = New ItemSpec("C:\DotNet TFS\TestPlugin1\Plugin1\test2\readme2.txt"RecursionType.None)
                    newchange.DeletionId = 0
                    newchange.ItemType = ItemType.Any
                    newchange.Encoding = -2
     
                    ' Do a CheckOut
                    Dim changearr() As ChangeRequest = New ChangeRequest() {newchange}
                    Dim newcontext As TeamFoundationRequestContext
                    newcontext = RequestContext.ServiceHost.CreateSystemContext()
                    VCService.PendChanges(newcontext, WorkSpaceName, NotificationEventArgs.OwnerName, changearr, 0, 0, Nothing)
                    ' In order for this to show up in Studio, a Refresh of the folder is needed in the Context menu
    Wednesday, November 02, 2011 5:35 PM