Added common file dialog service; upgraded to Avalonia 11.3.7
This commit is contained in:
parent
76a31e15f4
commit
eec1092e97
5 changed files with 346 additions and 2 deletions
|
|
@ -23,7 +23,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Avalonia" Version="11.3.1" />
|
<PackageReference Include="Avalonia" Version="11.3.7" />
|
||||||
<PackageReference Include="MessageBox.Avalonia" Version="3.2.0" />
|
<PackageReference Include="MessageBox.Avalonia" Version="3.2.0" />
|
||||||
<PackageReference Include="Nuclex.Foundation" Version="1.3.0" />
|
<PackageReference Include="Nuclex.Foundation" Version="1.3.0" />
|
||||||
<PackageReference Include="Nullable" Version="1.3.1">
|
<PackageReference Include="Nullable" Version="1.3.1">
|
||||||
|
|
|
||||||
|
|
@ -47,4 +47,4 @@ using System.Runtime.InteropServices;
|
||||||
// Build Number
|
// Build Number
|
||||||
// Revision
|
// Revision
|
||||||
//
|
//
|
||||||
[assembly: AssemblyVersion("1.3.3")]
|
[assembly: AssemblyVersion("1.4.0")]
|
||||||
|
|
|
||||||
232
Source/CommonDialogs/AvaloniaFileSelector.cs
Normal file
232
Source/CommonDialogs/AvaloniaFileSelector.cs
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
#region Apache License 2.0
|
||||||
|
/*
|
||||||
|
Nuclex Foundation libraries for .NET
|
||||||
|
Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
#endregion // Apache License 2.0
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
|
||||||
|
namespace Nuclex.Avalonia.CommonDialogs {
|
||||||
|
|
||||||
|
/// <summary>Displays the file picked dialog using the Avalonia interfaces</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The <see cref="IActiveWindowTracker" /> interface is used to determine the topmost
|
||||||
|
/// window of your application. so you either need to implement said interface or
|
||||||
|
/// build an MVVM application based on the <see cref="WindowManager" /> class which
|
||||||
|
/// will automatically provide an implementation of this interface and track which
|
||||||
|
/// dialog (or main window) of the application is active.
|
||||||
|
/// </remarks>
|
||||||
|
public class AvaloniaFileDialogs : IFilePickerService, IDirectoryPickerService {
|
||||||
|
|
||||||
|
/// <summary>Initialzies a new Avalonia-based file selector service</summary>
|
||||||
|
/// <param name="activeWindowTracker">
|
||||||
|
/// Used to determine the currently active window to which the file selector
|
||||||
|
/// dialog will be parented
|
||||||
|
/// </param>
|
||||||
|
public AvaloniaFileDialogs(IActiveWindowTracker activeWindowTracker) {
|
||||||
|
this.activeWindowTracker = activeWindowTracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Shows a file selector asking the user where to save a file</summary>
|
||||||
|
/// <param name="caption">
|
||||||
|
/// Caption to use for the dialog asking the user for a save location
|
||||||
|
/// </param>
|
||||||
|
/// <param name="fileTypes">File types the user can choose to save as</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A task that will provide the pre-opened file for saving or null if
|
||||||
|
/// the user has canceled the file picker dialog
|
||||||
|
/// </returns>
|
||||||
|
public Task<IStorageFile?> AskForSaveLocationAsync(
|
||||||
|
string caption, params FilePickerFileType[] fileTypes
|
||||||
|
) {
|
||||||
|
var options = new FilePickerSaveOptions() {
|
||||||
|
Title = caption,
|
||||||
|
ShowOverwritePrompt = true,
|
||||||
|
FileTypeChoices = fileTypes,
|
||||||
|
};
|
||||||
|
|
||||||
|
string? firstExtensionMentioned = findFirstExtensionMentioned(fileTypes);
|
||||||
|
if(firstExtensionMentioned != null) {
|
||||||
|
options.DefaultExtension = firstExtensionMentioned;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The currently active window / view in which the user must have clicked
|
||||||
|
// on some kind of button or menu that triggered the open file(s) dialog
|
||||||
|
Window? askingWindow = this.activeWindowTracker.ActiveWindow;
|
||||||
|
if(askingWindow == null) {
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Active window tracker did not provide an active Avalonia window. " +
|
||||||
|
"Was the file selector perhaps used in a headless app or in a unit test?"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the Avalonia file picker dialog. Avalonia will return null if
|
||||||
|
// the user cancels the dialog, which matches our own interface contract.
|
||||||
|
return askingWindow.StorageProvider.SaveFilePickerAsync(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Shows a file selector asking the user for a file to open</summary>
|
||||||
|
/// <param name="caption">
|
||||||
|
/// Caption to use for the dialog asking the user to select a file
|
||||||
|
/// </param>
|
||||||
|
/// <param name="fileTypes">File types the list is filtered for by default</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A task that will provide the pre-opened file for readiong or null if
|
||||||
|
/// the user has canceled the file picker dialog
|
||||||
|
/// </returns>
|
||||||
|
public async Task<IStorageFile?> AskForFileToOpeAsync(
|
||||||
|
string caption, params FilePickerFileType[] fileTypes
|
||||||
|
) {
|
||||||
|
var options = new FilePickerOpenOptions() {
|
||||||
|
Title = caption,
|
||||||
|
AllowMultiple = false,
|
||||||
|
FileTypeFilter = fileTypes
|
||||||
|
};
|
||||||
|
|
||||||
|
// The currently active window / view in which the user must have clicked
|
||||||
|
// on some kind of button or menu that triggered the open file(s) dialog
|
||||||
|
Window? askingWindow = this.activeWindowTracker.ActiveWindow;
|
||||||
|
if(askingWindow == null) {
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Active window tracker did not provide an active Avalonia window. " +
|
||||||
|
"Was the file selector perhaps used in a headless app or in a unit test?"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the Avalonia file picker dialog. Avalonia will return an empty list
|
||||||
|
// if the user cancels the dialog, but we want to be explicit, so if
|
||||||
|
// the list is either null (defensive programming) or contains no items,
|
||||||
|
// we will explicitly return a null result instead of an empty list.
|
||||||
|
IReadOnlyList<IStorageFile> selectedFile = (
|
||||||
|
await askingWindow.StorageProvider.OpenFilePickerAsync(options)
|
||||||
|
);
|
||||||
|
if((selectedFile == null) || (selectedFile.Count != 1)) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return selectedFile[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Shows a file selector asking the user for a file to open</summary>
|
||||||
|
/// <param name="caption">
|
||||||
|
/// Caption to use for the dialog asking the user to select a file
|
||||||
|
/// </param>
|
||||||
|
/// <param name="fileTypes">File types the list is filtered for by default</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A task that will provide the pre-opened file for readiong or null if
|
||||||
|
/// the user has canceled the file picker dialog
|
||||||
|
/// </returns>
|
||||||
|
public async Task<IReadOnlyList<IStorageFile>?> AskForFilesToOpenAsync(
|
||||||
|
string caption, params FilePickerFileType[] fileTypes
|
||||||
|
) {
|
||||||
|
var options = new FilePickerOpenOptions() {
|
||||||
|
Title = caption,
|
||||||
|
AllowMultiple = true,
|
||||||
|
FileTypeFilter = fileTypes
|
||||||
|
};
|
||||||
|
|
||||||
|
// The currently active window / view in which the user must have clicked
|
||||||
|
// on some kind of button or menu that triggered the open file(s) dialog
|
||||||
|
Window? askingWindow = this.activeWindowTracker.ActiveWindow;
|
||||||
|
if(askingWindow == null) {
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Active window tracker did not provide an active Avalonia window. " +
|
||||||
|
"Was the file selector perhaps used in a headless app or in a unit test?"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the Avalonia file picker dialog. Avalonia will return an empty list
|
||||||
|
// if the user cancels the dialog, but we want to be explicit, so if
|
||||||
|
// the list is either null (defensive programming) or contains no items,
|
||||||
|
// we will explicitly return a null result instead of an empty list.
|
||||||
|
IReadOnlyList<IStorageFile> selectedFiles = (
|
||||||
|
await askingWindow.StorageProvider.OpenFilePickerAsync(options)
|
||||||
|
);
|
||||||
|
if((selectedFiles == null) || (selectedFiles.Count == 0)) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return selectedFiles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Asks the user to select a directory</summary>
|
||||||
|
/// <returns>A task that will provide the directory the user has selected</returns>
|
||||||
|
public async Task<IStorageFolder?> AskForDirectory(string caption) {
|
||||||
|
var options = new FolderPickerOpenOptions() {
|
||||||
|
Title = caption,
|
||||||
|
AllowMultiple = false
|
||||||
|
};
|
||||||
|
|
||||||
|
// The currently active window / view in which the user must have clicked
|
||||||
|
// on some kind of button or menu that triggered the open directory dialog
|
||||||
|
Window? askingWindow = this.activeWindowTracker.ActiveWindow;
|
||||||
|
if(askingWindow == null) {
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Active window tracker did not provide an active Avalonia window. " +
|
||||||
|
"Was the directory selector perhaps used in a headless app or in a unit test?"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the Avalonia folder picker dialog. Avalonia will return an empty list
|
||||||
|
// if the user cancels the dialog, but we want to be explicit, so if
|
||||||
|
// the list is either null (defensive programming) or contains no items,
|
||||||
|
// we will explicitly return a null result instead of an empty list.
|
||||||
|
IReadOnlyList<IStorageFolder> selectedFolders = (
|
||||||
|
await askingWindow.StorageProvider.OpenFolderPickerAsync(options)
|
||||||
|
);
|
||||||
|
if((selectedFolders == null) || (selectedFolders.Count != 1)) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return selectedFolders[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Looks for the first file extension mentioned in the filters</summary>
|
||||||
|
/// <param name="fileTypes">Filters to search for the first mentioned extension</param>
|
||||||
|
/// <returns>The first extension mentioned or null if none are mentioned</returns>
|
||||||
|
private static string? findFirstExtensionMentioned(FilePickerFileType[] fileTypes) {
|
||||||
|
for(int index = 0; index < fileTypes.Length; ++index) {
|
||||||
|
IReadOnlyList<string>? patterns = fileTypes[index].Patterns;
|
||||||
|
if(patterns != null) {
|
||||||
|
int count = patterns.Count;
|
||||||
|
for(int patternIndex = 0; patternIndex < count; ++patternIndex) {
|
||||||
|
if(!string.IsNullOrEmpty(patterns[patternIndex])) {
|
||||||
|
string pattern = patterns[patternIndex];
|
||||||
|
int finalDotIndex = pattern.LastIndexOf('.');
|
||||||
|
if(finalDotIndex != -1) {
|
||||||
|
return patterns[patternIndex].Substring(finalDotIndex + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // for each pattern index
|
||||||
|
} // if patterns set
|
||||||
|
} // for each file type index
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides the active window the file picked dialog should become a child of
|
||||||
|
/// </summary>
|
||||||
|
private readonly IActiveWindowTracker activeWindowTracker;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Nuclex.Avalonia.CommonDialogs
|
||||||
36
Source/CommonDialogs/IDirectoryPickerService.cs
Normal file
36
Source/CommonDialogs/IDirectoryPickerService.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
#region Apache License 2.0
|
||||||
|
/*
|
||||||
|
Nuclex Foundation libraries for .NET
|
||||||
|
Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
#endregion // Apache License 2.0
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
|
||||||
|
namespace Nuclex.Avalonia.CommonDialogs {
|
||||||
|
|
||||||
|
/// <summary>Service that asks the user to pick a directory</summary>
|
||||||
|
public interface IDirectoryPickerService {
|
||||||
|
|
||||||
|
/// <summary>Asks the user to select a directory</summary>
|
||||||
|
/// <returns>A task that will provide the directory the user has selected</returns>
|
||||||
|
Task<IStorageFolder?> AskForDirectory(string caption);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Nuclex.Avalonia.CommonDialogs
|
||||||
76
Source/CommonDialogs/IFilePickerService.cs
Normal file
76
Source/CommonDialogs/IFilePickerService.cs
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
#region Apache License 2.0
|
||||||
|
/*
|
||||||
|
Nuclex Foundation libraries for .NET
|
||||||
|
Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
#endregion // Apache License 2.0
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
|
||||||
|
namespace Nuclex.Avalonia.CommonDialogs {
|
||||||
|
|
||||||
|
/// <summary>Service that asks the user to pick a file to open or to save</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// All methods here work with Avalonia's <see cref="IStorageFile" /> interface to
|
||||||
|
/// support web and mobile platforms.
|
||||||
|
/// </remarks>
|
||||||
|
public interface IFilePickerService {
|
||||||
|
|
||||||
|
/// <summary>Shows a file selector asking the user where to save a file</summary>
|
||||||
|
/// <param name="caption">
|
||||||
|
/// Caption to use for the dialog asking the user for a save location
|
||||||
|
/// </param>
|
||||||
|
/// <param name="fileTypes">File types the user can choose to save as</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A task that will provide the pre-opened file for saving or null if
|
||||||
|
/// the user has canceled the file picker dialog
|
||||||
|
/// </returns>
|
||||||
|
Task<IStorageFile?> AskForSaveLocationAsync(
|
||||||
|
string caption, params FilePickerFileType[] fileTypes
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>Shows a file selector asking the user for a file to open</summary>
|
||||||
|
/// <param name="caption">
|
||||||
|
/// Caption to use for the dialog asking the user to select a file
|
||||||
|
/// </param>
|
||||||
|
/// <param name="fileTypes">File types the list is filtered for by default</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A task that will provide the pre-opened file for readiong or null if
|
||||||
|
/// the user has canceled the file picker dialog
|
||||||
|
/// </returns>
|
||||||
|
Task<IStorageFile?> AskForFileToOpeAsync(
|
||||||
|
string caption, params FilePickerFileType[] fileTypes
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>Shows a file selector asking the user for a file to open</summary>
|
||||||
|
/// <param name="caption">
|
||||||
|
/// Caption to use for the dialog asking the user to select a file
|
||||||
|
/// </param>
|
||||||
|
/// <param name="fileTypes">File types the list is filtered for by default</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A task that will provide the pre-opened file for readiong or null if
|
||||||
|
/// the user has canceled the file picker dialog
|
||||||
|
/// </returns>
|
||||||
|
Task<IReadOnlyList<IStorageFile>?> AskForFilesToOpenAsync(
|
||||||
|
string caption, params FilePickerFileType[] fileTypes
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Nuclex.Avalonia.CommonDialogs
|
||||||
Loading…
Add table
Add a link
Reference in a new issue