问题
I need to create GUI that shows all available folders and files structure for my TFS project, for particular folder. For example: I have "DiagnosticsFolder" like on screenshot:
And I need to show Tree with project structure under required folder, including these files and folders ChangeType
(for instance: edited, edited by another user, added, deleted etcetera).
I found a lot of partial solutions, offereing to use some methods, however I haven't found full solution, and it is fairly challenging to determine files and folders status (ChangeType
) too.
In need something like that:
回答1:
I've done solution myself. It includes following parts: 1). View Models that represents Source Control Item:
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). Repository with recursive method that build the structure tree:
/// <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;
}
I've made it so complex because I had to use different method to extract information for files without changes, with changes and with changes that were made by another user. The method that allows async usage: public Task> BuildSourceControlStructureAsync(string folderPath = ConstDefaultFlowsTfsPath) { return Task.Run(() => BuildSourceControlStructure(folderPath)); }
3). Source Control View model:
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). Resources (datatemplates for 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>
In XAML you can find different approaches of data presentation by triggers and bindings.
There is result of the code above. It represents all states by different icons, UI changes and tooltips with information.
来源:https://stackoverflow.com/questions/38219680/c-sharp-tfs-api-show-project-structure-with-folders-and-files-including-their