Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -287,4 +287,9 @@ __pycache__/
*.odx.cs
*.xsd.cs

NuGet/
NuGet/

.claude/
.nuget-apikey
# The private XrmToolBox instance xtb.ps1 builds, including its connection and its token cache.
.xtb/
15 changes: 7 additions & 8 deletions DocTemplateManagerControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ public partial class DocTemplateManagerControl : PluginControlBase, IStatusBarMe
private EditTemplateControl _editControl = null;
private UploadMultipleSummary _uploadSummary = null;

// Where the last download went, so the picker reopens there instead of at This PC.
private string _lastDownloadFolder = null;

public DocTemplateManagerControl()
{
InitializeComponent();
Expand Down Expand Up @@ -292,16 +295,12 @@ private void PerformDeleteTemplates()
private void PerformDownloadTemplates()
{
// choose a folder for the save...
var folderDlg = new FolderBrowserDialog()
{
Description = "Select a destination folder for your download",
RootFolder = Environment.SpecialFolder.MyComputer,
ShowNewFolderButton = true
};
var saveFolder = FolderPicker.Pick(this, "Select a destination folder for your download", _lastDownloadFolder);

if ((folderDlg.ShowDialog() == DialogResult.OK) && (folderDlg.SelectedPath != null))
if (saveFolder != null)
{
ExecuteMethod(DownloadDocumentTemplates, folderDlg.SelectedPath);
_lastDownloadFolder = saveFolder;
ExecuteMethod(DownloadDocumentTemplates, saveFolder);
}
}

Expand Down
3 changes: 2 additions & 1 deletion Futurez.Xrm.Tools.DocTemplateManager.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Futurez.Xrm.Tools</RootNamespace>
<AssemblyName>Futurez.Xrm.Tools.DocTemplateManager</AssemblyName>
<TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<SccProjectName>SAK</SccProjectName>
<SccLocalPath>SAK</SccLocalPath>
Expand Down Expand Up @@ -233,6 +233,7 @@
</Compile>
<Compile Include="Helper\entity.partial.cs" />
<Compile Include="Helper\FileUpload.cs" />
<Compile Include="Helper\FolderPicker.cs" />
<Compile Include="Helper\ListViewItemComparer.cs" />
<Compile Include="Helper\WordHelper.cs" />
<Compile Include="LocalTemplateUpdaterDialog.cs">
Expand Down
170 changes: 170 additions & 0 deletions Helper/FolderPicker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace Futurez.Xrm.Tools
{
/// <summary>
/// Folder picker built on the Vista-era IFileOpenDialog, i.e. the same Explorer window
/// users see everywhere else, with a path bar, search and the usual navigation pane.
/// WinForms' <see cref="FolderBrowserDialog"/> on .NET Framework is still the old
/// SHBrowseForFolder tree, so it is only kept here as a fallback.
/// </summary>
internal static class FolderPicker
{
/// <summary>Returns the picked folder, or null when the user cancelled.</summary>
public static string Pick(IWin32Window owner, string title, string initialFolder)
{
try
{
return PickVista(owner, title, initialFolder);
}
catch (Exception)
{
// Anything odd about the shell (older OS, blocked COM) falls back to the old dialog.
return PickLegacy(title, initialFolder);
}
}

private static string PickVista(IWin32Window owner, string title, string initialFolder)
{
var dialog = (IFileOpenDialog)new FileOpenDialog();
try
{
dialog.SetOptions(FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST);
dialog.SetTitle(title);

var start = ExistingFolder(initialFolder);
if (start != null)
{
object item;
if (SHCreateItemFromParsingName(start, IntPtr.Zero, typeof(IShellItem).GUID, out item) == 0)
{
dialog.SetFolder((IShellItem)item);
}
Comment on lines +41 to +45
}

var hwnd = owner != null ? owner.Handle : IntPtr.Zero;
if (dialog.Show(hwnd) != 0)
{
return null; // cancelled
}
Comment on lines +48 to +52

IShellItem result;
dialog.GetResult(out result);
string path;
result.GetDisplayName(SIGDN_FILESYSPATH, out path);
Marshal.ReleaseComObject(result);
return path;
}
finally
{
Marshal.ReleaseComObject(dialog);
}
}

private static string PickLegacy(string title, string initialFolder)
{
using (var dialog = new FolderBrowserDialog { Description = title, ShowNewFolderButton = true })
{
var start = ExistingFolder(initialFolder);
if (start != null)
{
dialog.SelectedPath = start;
}

return dialog.ShowDialog() == DialogResult.OK ? dialog.SelectedPath : null;
}
}

private static string ExistingFolder(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return null;
}

try
{
var full = Path.GetFullPath(path.Trim());
return Directory.Exists(full) ? full : null;
}
catch (Exception)
{
return null;
}
}

private const uint FOS_PICKFOLDERS = 0x00000020;
private const uint FOS_FORCEFILESYSTEM = 0x00000040;
private const uint FOS_PATHMUSTEXIST = 0x00000800;
private const uint SIGDN_FILESYSPATH = 0x80058000;

[DllImport("shell32.dll", CharSet = CharSet.Unicode, PreserveSig = true)]
private static extern int SHCreateItemFromParsingName(
[MarshalAs(UnmanagedType.LPWStr)] string path,
IntPtr bindingContext,
[MarshalAs(UnmanagedType.LPStruct)] Guid interfaceId,
[MarshalAs(UnmanagedType.Interface)] out object item);

[ComImport, Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")]
private class FileOpenDialog
{
}

/// <summary>
/// IModalWindow + IFileDialog + IFileOpenDialog flattened into one declaration: the
/// slots have to appear in vtable order, and the ones this tool never calls are
/// declared with blind arguments just to keep the layout right.
/// </summary>
[ComImport, Guid("d57c7288-d4ad-4768-be02-9d969532d960"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IFileOpenDialog
{
// IModalWindow
[PreserveSig]
int Show(IntPtr parent);

// IFileDialog
void SetFileTypes(uint fileTypes, IntPtr filterSpec);
void SetFileTypeIndex(uint fileType);
void GetFileTypeIndex(out uint fileType);
void Advise(IntPtr events, out uint cookie);
void Unadvise(uint cookie);
void SetOptions(uint options);
void GetOptions(out uint options);
void SetDefaultFolder(IShellItem folder);
void SetFolder(IShellItem folder);
void GetFolder(out IShellItem folder);
void GetCurrentSelection(out IShellItem item);
void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string name);
void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string name);
void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string title);
void SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string text);
void SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string label);
void GetResult(out IShellItem item);
void AddPlace(IShellItem place, int order);
void SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string extension);
void Close([MarshalAs(UnmanagedType.Error)] int result);
void SetClientGuid(ref Guid client);
void ClearClientData();
void SetFilter(IntPtr filter);

// IFileOpenDialog
void GetResults(out IntPtr items);
void GetSelectedItems(out IntPtr items);
}

[ComImport, Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IShellItem
{
void BindToHandler(IntPtr bindingContext, ref Guid handler, ref Guid interfaceId, out IntPtr result);
void GetParent(out IShellItem parent);
void GetDisplayName(uint name, [MarshalAs(UnmanagedType.LPWStr)] out string displayName);
void GetAttributes(uint mask, out uint attributes);
void Compare(IShellItem other, uint hint, out int order);
}
}
}
46 changes: 46 additions & 0 deletions build.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Builds the tool and drops it into the XrmToolBox you use for real work.
#
# Unlike the sibling tools this is the non-SDK, packages.config project inherited from
# upstream, so two things differ. Packages are restored by nuget.exe, because dotnet restore
# only understands PackageReference. And the build runs on the full MSBuild that ships with
# Visual Studio rather than on dotnet build, because the .resx files hold images and the SDK
# build refuses those (MSB3823/MSB3822) unless the project takes on a System.Resources.
# Extensions dependency that upstream does not have.

$ErrorActionPreference = "Stop"

$solution = Join-Path $PSScriptRoot "Futurez.Xrm.Tools.DocTemplateManager.sln"

if (-not (Test-Path (Join-Path $PSScriptRoot "packages"))) {
$nuget = Get-Command nuget -ErrorAction SilentlyContinue
if (-not $nuget) {
throw "packages\ is missing and nuget.exe is not on PATH. Get it from https://dist.nuget.org/win-x86-commandline/latest/nuget.exe, then re-run."
}
& $nuget.Source restore $solution
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}

$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"
$msbuild = $null
if (Test-Path $vswhere) {
$msbuild = & $vswhere -latest -prerelease -products * -requires Microsoft.Component.MSBuild `
-find "MSBuild\**\Bin\amd64\MSBuild.exe" | Select-Object -First 1
}
if (-not $msbuild) {
$msbuild = (Get-Command msbuild -ErrorAction SilentlyContinue).Source
}
if (-not $msbuild) {
throw "MSBuild was not found. Install Visual Studio or the Build Tools with the MSBuild component."
}

& $msbuild $solution /t:Build /p:Configuration=Debug /p:Platform="Any CPU" /v:minimal /nologo
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

$pluginDir = "C:\Users\kk\Downloads\XrmToolbox\Plugins"
if (-not (Test-Path $pluginDir)) { New-Item -ItemType Directory -Path $pluginDir | Out-Null }

# No target framework folder in the path: a non-SDK project writes straight into bin\Debug.
$source = Join-Path $PSScriptRoot "bin\Debug\Futurez.Xrm.Tools.DocTemplateManager.dll"
Copy-Item $source -Destination $pluginDir -Force

Write-Host "Built and deployed Futurez.Xrm.Tools.DocTemplateManager.dll to $pluginDir"
Binary file added docs/shots/dialog-after.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/shots/dialog-before.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/shots/grid-seeded.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test_doc.docx
Binary file not shown.
Loading