C# TFS API: показать структуру проекта с папками и файлами, включая их ChangeType (извлечен, удален, переименован), как в Visual Studio

Мне нужно создать графический интерфейс, который показывает все доступные папки и структуру файлов для моего проекта TFS, для конкретной папки. Например: у меня есть "DiagnosticsFolder", как на скриншоте:

И мне нужно показать дерево со структурой проекта в нужной папке, включая эти файлы и папки ChangeType (например: отредактировано, отредактировано другим пользователем, добавлено, удалено и т. д.).

Я нашел много частичных решений, предлагая использовать некоторые методы, однако я не нашел полного решения, и довольно сложно определить состояние файлов и папок (ChangeType) тоже.

При необходимости что-то подобное:

1 ответ

Я сделал решение сам. Включает в себя следующие части: 1). Просмотр моделей, представляющих элемент управления источником:

 public abstract class SourceControlItemViewBaseModel : ViewModelBase
{
    private string _name;
    public string Name
    {
        get { return _name; }
        set
        {
            _name = value;
            OnPropertyChanged();
        }
    }

    private string _localPath;
    public string LocalPath
    {
        get { return _localPath; }
        set
        {
            _localPath = value;
            OnPropertyChanged();
        }
    }

    private string _serverPath;
    public string ServerPath
    {
        get { return _serverPath; }
        set
        {
            _serverPath = value;
            OnPropertyChanged();
        }
    }

    private string _pendingSetName;
    /// <summary>
    /// Computer name where changes were made
    /// </summary>
    public string PendingSetName
    {
        get { return _pendingSetName; }
        set
        {
            _pendingSetName = value;
            OnPropertyChanged();
        }
    }

    private string _pendingSetOwner;
    /// <summary>
    /// User Name who made the changes
    /// </summary>
    public string PendingSetOwner
    {
        get { return _pendingSetOwner; }
        set
        {
            _pendingSetOwner = value;
            OnPropertyChanged();
        }
    }

    private string _toolTipText;
    public string ToolTipText
    {
        get { return _toolTipText; }
        set
        {
            _toolTipText = value;
            OnPropertyChanged();
        }
    }

    public string SourceServerItem
    {
        get { return _sourceServerItem; }
        set
        {
            _sourceServerItem = value;
            OnPropertyChanged();
        }
    }

    private SourceControlState _state;
    private string _sourceServerItem;

    public SourceControlState State
    {
        get { return _state; }
        set
        {
            _state = value;
            OnPropertyChanged();
        }
    }
}

public class SourceControlFileViewModel : SourceControlItemViewBaseModel
{
}

 public class SourceControlDirecoryViewModel : SourceControlItemViewBaseModel
{
    public List<SourceControlItemViewBaseModel> Items { get; set; }

    public SourceControlDirecoryViewModel()
    {
        Items = new List<SourceControlItemViewBaseModel>();
    }
}



[Flags]//My own Enum that represents different states
public enum SourceControlState
{
    Online = 0,
    CheckedOut = 1,
    Added = 2,
    Deleted = 4,
    Locked = 8,
    Renamed = 16
}

2). Репозиторий с рекурсивным методом, который строит дерево структуры:

 /// <summary>
    /// 
    /// </summary>
    /// <param name="serverPath">TFS source path</param>
    /// <param name="serverSourcePath">this path should be used in the case server item's name was changed. Needs to be used because TFS cannot get items from folder whose name has been changed</param>
    /// <returns></returns>
    public List<SourceControlItemViewBaseModel> BuildSourceControlStructure(string serverPath = ConstDefaultFlowsTfsPath, string serverSourcePath = null)
    {
        #region Local members
        var resultItems = new List<SourceControlItemViewBaseModel>();
        var workspaceInfo = Workstation.Current.GetLocalWorkspaceInfo(serverPath);
        var server = RegisteredTfsConnections.GetProjectCollection(workspaceInfo.ServerUri);
        var projects = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(server);
        var versionControl = (VersionControlServer)projects.GetService(typeof(VersionControlServer));
        var workspace = versionControl.GetWorkspace(Environment.MachineName, Environment.UserName);
        #endregion

        #region Extraction of folders information and pending changes
        //Info from Directories
        PendingChange[] changesFoldersByAnotherUser =
            versionControl.QueryPendingSets(new[] { serverPath }, RecursionType.OneLevel, null, null)//this method brings changes that were made by another user or through another machine, whereas workspace.GetPendingChanges() method doesn't.
                .SelectMany(pnd => pnd.PendingChanges)
                .Where(i => i.ItemType == ItemType.Folder && i.ServerItem != serverPath &&
                (i.PendingSetOwner != WindowsIdentity.GetCurrent()?.Name || i.PendingSetName != Environment.MachineName))
                .ToArray();

        PendingChange[] changesFolders = workspace.GetPendingChanges(serverPath, RecursionType.OneLevel)//needs to show changes under renamed folders
                .Where(i => i.ItemType == ItemType.Folder && i.ServerItem != serverPath)
                .ToArray();

        Item[] itemsFolders = versionControl.GetItems(serverPath, RecursionType.OneLevel).Items
         .Where(item => item.ItemType == ItemType.Folder && item.ServerItem != serverPath &&//needs to avoid duplicate presentation of folders
            changesFoldersByAnotherUser.All(chng => chng.ServerItem != item.ServerItem && chng.SourceServerItem != item.ServerItem) &&//needs to avoid duplicate presentation of folders
                changesFolders.All(chg => chg.ServerItem != item.ServerItem && chg.SourceServerItem != item.ServerItem)).ToArray();

        if (serverSourcePath != null)//means folder name has been changed, therefore items cannot be got through using serverPath
        {
            itemsFolders = versionControl.GetItems(serverSourcePath, RecursionType.OneLevel).Items
             .Where(item => item.ItemType == ItemType.Folder && item.ServerItem != serverSourcePath && changesFolders
                .All(chng => chng.ServerItem != item.ServerItem && chng.SourceServerItem != item.ServerItem)).ToArray();
        }
        #endregion

        #region Initialization of Items and sub items for folder
        //Folder item with changing
        foreach (Item folderItem in itemsFolders)
        {
            var vm = new SourceControlDirecoryViewModel
            {
                Name = Path.GetFileName(folderItem.ServerItem),
                LocalPath = workspace?.GetLocalItemForServerItem(folderItem.ServerItem),
                ServerPath = folderItem.ServerItem,
                Items = BuildSourceControlStructure(folderItem.ServerItem),
                PendingSetName = null,
                PendingSetOwner = null,
                SourceServerItem = null,
                ToolTipText = "Connected to TFS",
                State = SourceControlState.Online

            };
            resultItems.Add(vm);
        }

        foreach (PendingChange currentFolderChange in changesFolders)
        {
            var vm = new SourceControlDirecoryViewModel
            {
                Name = Path.GetFileName(currentFolderChange.ServerItem),
                LocalPath = workspace?.GetLocalItemForServerItem(currentFolderChange.ServerItem),
                ServerPath = currentFolderChange.ServerItem,
                Items = BuildSourceControlStructure(currentFolderChange.ServerItem, currentFolderChange.SourceServerItem),
                PendingSetName = currentFolderChange?.PendingSetName,
                PendingSetOwner = currentFolderChange?.PendingSetOwner,
                SourceServerItem = currentFolderChange?.SourceServerItem,
                ToolTipText = currentFolderChange?.ToolTipText,
                State = ConvertControlState(currentFolderChange)

            };
            resultItems.Add(vm);
        }

        foreach (PendingChange folderChangeByAnUser in changesFoldersByAnotherUser)
        {
            var vm = new SourceControlDirecoryViewModel
            {
                Name = Path.GetFileName(folderChangeByAnUser.ServerItem),
                LocalPath = workspace?.GetLocalItemForServerItem(folderChangeByAnUser.ServerItem),
                ServerPath = folderChangeByAnUser.ServerItem,
                Items = BuildSourceControlStructure(folderChangeByAnUser.ServerItem),
                PendingSetName = folderChangeByAnUser?.PendingSetName,
                PendingSetOwner = folderChangeByAnUser?.PendingSetOwner,
                SourceServerItem = folderChangeByAnUser?.SourceServerItem,
                ToolTipText = folderChangeByAnUser?.ToolTipText,
                State = ConvertControlState(folderChangeByAnUser)

            };
            resultItems.Add(vm);
        }
        #endregion

        #region Extraction of files information and pending changes
        PendingChange[] changesFilesByAnotherUser =
       versionControl.QueryPendingSets(new[] { serverPath }, RecursionType.OneLevel, null, null)//this method brings changes that were made by another user or through another machine, whereas workspace.GetPendingChanges() method doesn't.
           .SelectMany(pnd => pnd.PendingChanges)
           .Where(i => i.ItemType == ItemType.File && i.ServerItem != serverPath &&
              (i.PendingSetOwner != WindowsIdentity.GetCurrent()?.Name || i.PendingSetName != Environment.MachineName)
              && i.ServerItem.EndsWith(ConstTargetFileExtenstion))//filter files by extension
              .ToArray();


        PendingChange[] changesFiles = workspace.GetPendingChanges(serverPath, RecursionType.OneLevel)
             .Where(i => i.ItemType == ItemType.File && i.ServerItem != serverPath)
             .ToArray();


        Item[] itemsFiles = versionControl.GetItems(serverPath, RecursionType.OneLevel).Items
            .Where(item => item.ItemType == ItemType.File && item.ServerItem != serverPath && item.ServerItem.EndsWith(ConstTargetFileExtenstion) &&//filter files by extension
                changesFilesByAnotherUser.All(chng => chng.ServerItem != item.ServerItem && chng.SourceServerItem != item.ServerItem) &&//needs to avoid duplicate presentation of folders
                    changesFiles.All(chg => chg.ServerItem != item.ServerItem && chg.SourceServerItem != item.ServerItem)).ToArray();//needs to avoid duplicate presentation of folders

        if (serverSourcePath != null)//needs to use serverSourcePath(full server path before it was changed) to receive its content
        {
            itemsFiles = versionControl.GetItems(serverSourcePath, RecursionType.OneLevel).Items
                .Where(item => item.ItemType == ItemType.File && item.ServerItem != serverPath && item.ServerItem.EndsWith(ConstTargetFileExtenstion)//filter files by extension
                && changesFiles.All(chng => chng.ServerItem != item.ServerItem && chng.SourceServerItem != item.ServerItem))
                .ToArray();
        }
        #endregion

        #region Ininialization of Items and sub items for files
        foreach (Item fileItem in itemsFiles)
        {
            var vm = new SourceControlFileViewModel
            {
                Name = Path.GetFileName(fileItem.ServerItem),
                ServerPath = fileItem.ServerItem,
                LocalPath = workspace?.GetLocalItemForServerItem(fileItem.ServerItem),
                PendingSetName = null,
                PendingSetOwner = null,
                SourceServerItem = null,
                ToolTipText = "Connected to TFS",
                State = SourceControlState.Online
            };
            resultItems.Add(vm);
        }

        foreach (PendingChange currentFilePendChange in changesFiles)
        {
            var vm = new SourceControlFileViewModel
            {
                Name = Path.GetFileName(currentFilePendChange.ServerItem),
                ServerPath = currentFilePendChange.ServerItem,
                LocalPath = workspace?.GetLocalItemForServerItem(currentFilePendChange.ServerItem),
                PendingSetName = currentFilePendChange?.PendingSetName,
                PendingSetOwner = currentFilePendChange?.PendingSetOwner,
                SourceServerItem = currentFilePendChange?.SourceServerItem,
                ToolTipText = currentFilePendChange?.ToolTipText,
                State = ConvertControlState(currentFilePendChange)
            };
            resultItems.Add(vm);
        }

        foreach (PendingChange fileChangeByAnUser in changesFilesByAnotherUser)
        {
            SourceControlState state = ConvertControlState(fileChangeByAnUser);
            string localPath = workspace?.GetLocalItemForServerItem(state == (SourceControlState.Locked | SourceControlState.Renamed) ? fileChangeByAnUser?.SourceServerItem : fileChangeByAnUser.ServerItem);

            var vm = new SourceControlFileViewModel
            {
                Name = Path.GetFileName(localPath),
                ServerPath = fileChangeByAnUser?.ServerItem,
                LocalPath = localPath,
                PendingSetName = fileChangeByAnUser?.PendingSetName,
                PendingSetOwner = fileChangeByAnUser?.PendingSetOwner,
                SourceServerItem = fileChangeByAnUser?.SourceServerItem,
                ToolTipText = fileChangeByAnUser?.ToolTipText,
                State = state
            };
            resultItems.Add(vm);
        }
        #endregion

        return resultItems;
    }

Я сделал это настолько сложным, потому что мне пришлось использовать другой метод для извлечения информации для файлов без изменений, с изменениями и с изменениями, которые были сделаны другим пользователем. Метод, позволяющий использовать асинхронно: public Task> BuildSourceControlStructureAsync (string folderPath = ConstDefaultFlowsTfsPath) {return Task.Run (() => BuildSourceControlStructure (folderPath)); }

3). Модель Source Control View:

 public class SourceControlViewModel : ViewModelBase
    {

 public IEnumerable<SourceControlItemViewBaseModel> SourceControlStructureItems { get; set; }
    public async Task Init()
    {
        await SourceControlRepository.Instance.Init();
        //Here the strucure builds
        SourceControlStructureItems = await SourceControlRepository.Instance.BuildSourceControlStructureAsync();

    }
}

4). XAML:

  <TreeView ItemsSource="{Binding SourceControlStructureItems}" />

5). Ресурсы (шаблоны данных для TreeView):

 <HierarchicalDataTemplate DataType="{x:Type viewModels:SourceControlDirecoryViewModel}"
                              ItemsSource="{Binding Items}">
        <Grid x:Name="LayoutRoot">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="5" />
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="5" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <Image Width="9"
                   Height="9"
                   Grid.Column="0"
                   VerticalAlignment="Center"
                   HorizontalAlignment="Center"
                   ToolTipService.ShowOnDisabled="True"
                   x:Name="srcCtrlStatusIndicator">
                <FrameworkElement.ToolTip>
                    <ToolTip>
                        <TextBlock Text="{Binding State}" />
                    </ToolTip>
                </FrameworkElement.ToolTip>
            </Image>
            <Image Width="16"
                   Height="16"
                   VerticalAlignment="Center"
                   HorizontalAlignment="Center"
                   Source="{StaticResource ImageSourceFolderClosed16x16}"
                   x:Name="img"
                   Grid.Column="2" />
            <TextBlock Text="{Binding Path=Name}"
                       ToolTipService.ShowOnDisabled="True"
                       VerticalAlignment="Center"
                       Grid.Column="4"
                       x:Name="txt">
                <FrameworkElement.ToolTip>
                    <ToolTip>
                        <TextBlock Text="{Binding Path=LocalPath}"
                                   x:Name="txtToolTip" />
                    </ToolTip>
                </FrameworkElement.ToolTip>
            </TextBlock>
        </Grid>

        <DataTemplate.Triggers>
            <DataTrigger Binding="{Binding IsExpanded, RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type TreeViewItem}}}"
                         Value="True">
                <Setter Property="Source"
                        TargetName="img"
                        Value="{StaticResource ImageSourceFolderOpened16x16}" />
            </DataTrigger>
            <DataTrigger Binding="{Binding State}"
                         Value="Deleted">
                <Setter Property="TextBlock.TextDecorations"
                        TargetName="txt"
                        Value="Strikethrough" />
            </DataTrigger>
            <!--<DataTrigger Binding="{Binding State}"
                         Value="CheckedOut">
                <Setter Property="Source"
                        TargetName="srcCtrlStatusIndicator"
                        Value="{StaticResource ImageSourceFolderCheckedOut9x9}" />
            </DataTrigger>-->
            <DataTrigger Binding="{Binding State}"
                         Value="Added">
                <Setter Property="Source"
                        TargetName="srcCtrlStatusIndicator"
                        Value="{StaticResource ImageSourceFolderAdded9x9}" />
            </DataTrigger>
            <DataTrigger Binding="{Binding State}"
                         Value="Deleted">
                <Setter Property="Source"
                        TargetName="srcCtrlStatusIndicator"
                        Value="{StaticResource ImageSourceFolderRemove16x16}" />
                <Setter Property="TextBlock.TextDecorations"
                        TargetName="txt"
                        Value="Strikethrough" />
            </DataTrigger>
            <DataTrigger Binding="{Binding State}"
                         Value="Renamed">
                <Setter Property="Source"
                        TargetName="srcCtrlStatusIndicator"
                        Value="{StaticResource ImageSourceRenamed9x9}" />
                <Setter Property="Text"
                        TargetName="txt">
                    <Setter.Value>
                        <MultiBinding StringFormat="{}{0} [{1}]">
                            <Binding Path="ServerPath"
                                     Converter="{cnv:FullPathToShortNameConverter}"
                                     Mode="OneWay" />
                            <Binding Path="SourceServerItem"
                                     Converter="{cnv:FullPathToShortNameConverter}"
                                     Mode="OneWay" />
                        </MultiBinding>
                    </Setter.Value>
                </Setter>
                <Setter Property="Text"
                        TargetName="txtToolTip">
                    <Setter.Value>
                        <MultiBinding StringFormat="{}[{1}] &#x0a; was renamed to &#x0a; {0} ">
                            <Binding Path="ServerPath"
                                     Mode="OneWay" />
                            <Binding Path="SourceServerItem"
                                     Mode="OneWay" />
                        </MultiBinding>
                    </Setter.Value>
                </Setter>
            </DataTrigger>
            <DataTrigger Binding="{Binding State}"
                         Value="Locked">
                <Setter Property="Source"
                        TargetName="srcCtrlStatusIndicator"
                        Value="{StaticResource ImageSourceCheckedOutBySomeoneElse9x9}" />
                <Setter Property="Opacity"
                        TargetName="txt"
                        Value="0.5" />
                <Setter Property="Text"
                        TargetName="txtToolTip">
                    <Setter.Value>
                        <MultiBinding StringFormat="{} Folder is readonly and uneditable. &#x0a; Reason: it has been checked out by: [{1}] &#x0a; On machine: {0}  ">
                            <Binding Path="PendingSetName"
                                     Mode="OneWay" />
                            <Binding Path="PendingSetOwner"
                                     Mode="OneWay" />
                        </MultiBinding>
                    </Setter.Value>
                </Setter>
            </DataTrigger>
        </DataTemplate.Triggers>
    </HierarchicalDataTemplate>

    <DataTemplate DataType="{x:Type viewModels:SourceControlFileViewModel}">
        <Grid x:Name="LayoutRoot">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="5" />
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="5" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <Image Width="9"
                   Height="9"
                   ToolTipService.ShowOnDisabled="True"
                   VerticalAlignment="Center"
                   HorizontalAlignment="Center"
                   Source="{StaticResource ImageSourceFolderUnderSourceControl9x9}"
                   x:Name="srcCtrlStatusIndicator">
                <FrameworkElement.ToolTip>
                    <ToolTip>
                        <TextBlock Text="{Binding State}" />
                    </ToolTip>
                </FrameworkElement.ToolTip>

            </Image>
            <Image Width="16"
                   Height="16"
                   Grid.Column="2"
                   VerticalAlignment="Center"
                   Source="{StaticResource ImageSourceFolderXaml16x16}" />
            <TextBlock Text="{Binding Path=Name}"
                       ToolTipService.ShowOnDisabled="True"
                       VerticalAlignment="Center"
                       Grid.Column="4"
                       x:Name="txt">
                <FrameworkElement.ToolTip>
                    <ToolTip>
                        <TextBlock Text="{Binding Path=LocalPath}"
                                   x:Name="txtToolTip" />
                    </ToolTip>
                </FrameworkElement.ToolTip>
            </TextBlock>
        </Grid>
        <DataTemplate.Triggers>
            <DataTrigger Binding="{Binding State}"
                         Value="CheckedOut">
                <Setter Property="Source"
                        TargetName="srcCtrlStatusIndicator"
                        Value="{StaticResource ImageSourceFolderCheckedOut9x9}" />
            </DataTrigger>
            <DataTrigger Binding="{Binding State}"
                         Value="Added">
                <Setter Property="Source"
                        TargetName="srcCtrlStatusIndicator"
                        Value="{StaticResource ImageSourceFolderAdded9x9}" />
            </DataTrigger>
            <DataTrigger Binding="{Binding State}"
                         Value="Deleted">
                <Setter Property="Source"
                        TargetName="srcCtrlStatusIndicator"
                        Value="{StaticResource ImageSourceFolderRemove16x16}" />
                <Setter Property="TextBlock.TextDecorations"
                        TargetName="txt"
                        Value="Strikethrough" />
            </DataTrigger>
            <DataTrigger Binding="{Binding State}"
                         Value="Renamed">
                <Setter Property="Source"
                        TargetName="srcCtrlStatusIndicator"
                        Value="{StaticResource ImageSourceRenamed9x9}" />
                <Setter Property="Text"
                        TargetName="txt">
                    <Setter.Value>
                        <MultiBinding StringFormat="{}{0} [{1}]">
                            <Binding Path="ServerPath"
                                     Converter="{cnv:FullPathToShortNameConverter}"
                                     Mode="OneWay" />
                            <Binding Path="SourceServerItem"
                                     Converter="{cnv:FullPathToShortNameConverter}"
                                     Mode="OneWay" />
                        </MultiBinding>
                    </Setter.Value>
                </Setter>
                <Setter Property="Text"
                        TargetName="txtToolTip">
                    <Setter.Value>
                        <MultiBinding StringFormat="{}[{1}] &#x0a; was renamed to &#x0a; {0} ">
                            <Binding Path="ServerPath"
                                     Mode="OneWay" />
                            <Binding Path="SourceServerItem"
                                     Mode="OneWay" />
                        </MultiBinding>
                    </Setter.Value>
                </Setter>
            </DataTrigger>
            <DataTrigger Binding="{Binding State,Converter={cnv:EnumFlagConverter FlagValue='Locked'}, ConverterParameter={x:Type viewModels:SourceControlState}}"
                         Value="True">
                <Setter Property="Source"
                        TargetName="srcCtrlStatusIndicator"
                        Value="{StaticResource ImageSourceCheckedOutBySomeoneElse9x9}" />
                <Setter Property="Opacity"
                        TargetName="txt"
                        Value="0.5" />
                <Setter Property="Text"
                        TargetName="txtToolTip">
                    <Setter.Value>
                        <MultiBinding StringFormat="{} File is readonly and uneditable. &#x0a; Reason: it has been checked out by: [{1}] &#x0a; On machine {0}&#x0a; Change Type is {2}">
                            <Binding Path="PendingSetName"
                                     Mode="OneWay" />
                            <Binding Path="PendingSetOwner"
                                     Mode="OneWay" />
                            <Binding Path="State"
                                     Mode="OneWay" />
                        </MultiBinding>
                    </Setter.Value>
                </Setter>
            </DataTrigger>
        </DataTemplate.Triggers>
    </DataTemplate>

В XAML вы можете найти различные подходы к представлению данных с помощью триггеров и привязок.

Есть результат кода выше. Он представляет все состояния различными значками, изменениями пользовательского интерфейса и подсказками с информацией.

Другие вопросы по тегам