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}] 
 was renamed to 
 {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. 
 Reason: it has been checked out by: [{1}] 
 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}] 
 was renamed to 
 {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. 
 Reason: it has been checked out by: [{1}] 
 On machine {0}
 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 вы можете найти различные подходы к представлению данных с помощью триггеров и привязок.
Есть результат кода выше. Он представляет все состояния различными значками, изменениями пользовательского интерфейса и подсказками с информацией.