diff --git a/AIDevGallery.Tests/UnitTests/Samples/ResourceLifecycleTests.cs b/AIDevGallery.Tests/UnitTests/Samples/ResourceLifecycleTests.cs
new file mode 100644
index 00000000..3e12a2f8
--- /dev/null
+++ b/AIDevGallery.Tests/UnitTests/Samples/ResourceLifecycleTests.cs
@@ -0,0 +1,114 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System.Threading.Tasks;
+using Windows.Graphics.Imaging;
+
+namespace AIDevGallery.Tests.UnitTests.Samples;
+
+///
+/// Tests for BackgroundRemover's SetImageSource method to verify bitmap lifecycle management
+///
+[TestClass]
+public class BitmapLifecycleTests
+{
+ [TestMethod]
+ public void SoftwareBitmapConvertCreatesIndependentCopy()
+ {
+ // This test verifies that SoftwareBitmap.Convert creates an independent copy
+ // and that disposing the original doesn't affect the converted bitmap.
+ // This is critical for BackgroundRemover's lifecycle management.
+
+ // Arrange
+ var originalBitmap = new SoftwareBitmap(
+ BitmapPixelFormat.Rgba8,
+ 100,
+ 100,
+ BitmapAlphaMode.Premultiplied);
+
+ // Act
+ var convertedBitmap = SoftwareBitmap.Convert(
+ originalBitmap,
+ BitmapPixelFormat.Bgra8,
+ BitmapAlphaMode.Premultiplied);
+
+ // Assert - Verify it's a new instance with correct properties
+ Assert.IsNotNull(convertedBitmap, "Converted bitmap should not be null");
+ Assert.AreNotSame(originalBitmap, convertedBitmap, "Convert should create a new instance");
+ Assert.AreEqual(originalBitmap.PixelWidth, convertedBitmap.PixelWidth, "Width should match");
+ Assert.AreEqual(originalBitmap.PixelHeight, convertedBitmap.PixelHeight, "Height should match");
+ Assert.AreEqual(BitmapPixelFormat.Bgra8, convertedBitmap.BitmapPixelFormat, "Format should be converted");
+
+ // Dispose original - this should NOT affect the converted bitmap
+ originalBitmap.Dispose();
+
+ // Assert - converted bitmap should still be usable after disposing original
+ Assert.AreEqual(100, convertedBitmap.PixelWidth, "Converted bitmap should remain valid");
+ Assert.AreEqual(100, convertedBitmap.PixelHeight, "Converted bitmap should remain valid");
+
+ // Clean up
+ convertedBitmap.Dispose();
+ }
+
+ [TestMethod]
+ public async Task BackgroundRemoverBitmapLifecycleVerifyCorrectPattern()
+ {
+ // *** CRITICAL TEST FOR BackgroundRemover.xaml.cs ***
+ // This test validates the exact pattern used in BackgroundRemover:
+ //
+ // Pattern in BackgroundRemover.xaml.cs:
+ // using (var outputBitmap = await ExtractBackground(...))
+ // {
+ // await SetImageSource(outputBitmap); // Converts and uses bitmap
+ // } // outputBitmap is disposed here
+ //
+ // Key Question: Is it safe to dispose outputBitmap after SetImageSource?
+ // Answer: YES, because SoftwareBitmap.Convert creates an independent copy.
+ //
+ // This test verifies that the converted bitmap remains valid even after
+ // the original outputBitmap is disposed, ensuring no use-after-free bugs.
+
+ // Arrange - Create a bitmap (simulating ExtractBackground result)
+ SoftwareBitmap? outputBitmap = null;
+ SoftwareBitmap? convertedBitmap = null;
+
+ try
+ {
+ outputBitmap = new SoftwareBitmap(
+ BitmapPixelFormat.Rgba8,
+ 200,
+ 200,
+ BitmapAlphaMode.Premultiplied);
+
+ // Act - Simulate SetImageSource logic
+ // Inside SetImageSource, this happens:
+ convertedBitmap = SoftwareBitmap.Convert(
+ outputBitmap,
+ BitmapPixelFormat.Bgra8,
+ BitmapAlphaMode.Premultiplied);
+
+ // At this point in BackgroundRemover, we would:
+ // await bitmapSource.SetBitmapAsync(convertedBitmap);
+ // The SetBitmapAsync call completes synchronously with the bitmap data
+ Assert.IsNotNull(convertedBitmap, "Converted bitmap should be created");
+ Assert.AreEqual(200, convertedBitmap.PixelWidth, "Initial width should be correct");
+
+ // Now simulate the using block exiting - dispose outputBitmap
+ // This is the CRITICAL moment: does disposing outputBitmap break convertedBitmap?
+ outputBitmap.Dispose();
+
+ // Assert - convertedBitmap should still be valid because it's an independent copy
+ Assert.AreEqual(200, convertedBitmap.PixelWidth, "Width should still be accessible");
+ Assert.AreEqual(200, convertedBitmap.PixelHeight, "Height should still be accessible");
+ Assert.AreEqual(BitmapPixelFormat.Bgra8, convertedBitmap.BitmapPixelFormat, "Format should still be accessible");
+
+ await Task.CompletedTask; // Simulate async operation
+ }
+ finally
+ {
+ // Clean up - in real code, the UI framework holds the convertedBitmap reference
+ convertedBitmap?.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/AIDevGallery/AIDevGallery.csproj b/AIDevGallery/AIDevGallery.csproj
index faeaca95..94957f6a 100644
--- a/AIDevGallery/AIDevGallery.csproj
+++ b/AIDevGallery/AIDevGallery.csproj
@@ -17,8 +17,6 @@
$(NoWarn);IL2050
$(NoWarn);IL2026
-
- $(NoWarn);IDISP001;IDISP002;IDISP003;IDISP004;IDISP006;IDISP007;IDISP008;IDISP017;IDISP025
true
SamplesRoots.xml
diff --git a/AIDevGallery/Controls/DownloadProgressList.xaml.cs b/AIDevGallery/Controls/DownloadProgressList.xaml.cs
index 0baf849b..bb35633a 100644
--- a/AIDevGallery/Controls/DownloadProgressList.xaml.cs
+++ b/AIDevGallery/Controls/DownloadProgressList.xaml.cs
@@ -72,7 +72,11 @@ private void RetryDownloadClicked(object sender, RoutedEventArgs e)
if (sender is Button button && button.Tag is DownloadableModel downloadableModel)
{
downloadProgresses.Remove(downloadableModel);
+
+ // ModelDownload lifecycle is managed by ModelDownloadQueue, should not be disposed immediately
+#pragma warning disable IDISP004 // Don't ignore created IDisposable
App.ModelDownloadQueue.AddModel(downloadableModel.ModelDetails);
+#pragma warning restore IDISP004
}
}
@@ -93,7 +97,11 @@ private void VerificationFailedClicked(object sender, RoutedEventArgs e)
if (sender is Button button && button.Tag is DownloadableModel downloadableModel)
{
downloadProgresses.Remove(downloadableModel);
+
+ // ModelDownload lifecycle is managed by ModelDownloadQueue, should not be disposed immediately
+#pragma warning disable IDISP004 // Don't ignore created IDisposable
App.ModelDownloadQueue.AddModel(downloadableModel.ModelDetails);
+#pragma warning restore IDISP004
}
}
}
\ No newline at end of file
diff --git a/AIDevGallery/Controls/HomePage/Header/Lights/AmbLight.cs b/AIDevGallery/Controls/HomePage/Header/Lights/AmbLight.cs
index e69a53f2..b9600e11 100644
--- a/AIDevGallery/Controls/HomePage/Header/Lights/AmbLight.cs
+++ b/AIDevGallery/Controls/HomePage/Header/Lights/AmbLight.cs
@@ -14,10 +14,19 @@ internal partial class AmbLight : XamlLight
protected override void OnConnected(UIElement newElement)
{
+ // Compositor is a shared system resource and should not be disposed
+#pragma warning disable IDISP001 // Dispose created
Compositor compositor = CompositionTarget.GetCompositorForCurrentThread();
+#pragma warning restore IDISP001
+
+ // Dispose previous CompositionLight if exists
+ CompositionLight?.Dispose();
// Create AmbientLight and set its properties
+ // Ownership of ambientLight is transferred to CompositionLight property
+#pragma warning disable IDISP001 // Dispose created
AmbientLight ambientLight = compositor.CreateAmbientLight();
+#pragma warning restore IDISP001
ambientLight.Color = Colors.White;
// Associate CompositionLight with XamlLight
diff --git a/AIDevGallery/Controls/HomePage/Header/Lights/HoverLight.cs b/AIDevGallery/Controls/HomePage/Header/Lights/HoverLight.cs
index b23606dc..e2dec2fd 100644
--- a/AIDevGallery/Controls/HomePage/Header/Lights/HoverLight.cs
+++ b/AIDevGallery/Controls/HomePage/Header/Lights/HoverLight.cs
@@ -11,41 +11,52 @@
namespace AIDevGallery.Controls;
-internal partial class HoverLight : XamlLight
+internal sealed partial class HoverLight : XamlLight, IDisposable
{
private ExpressionAnimation? _lightPositionExpression;
private Vector3KeyFrameAnimation? _offsetAnimation;
+ private SpotLight? _spotLight;
+ private CompositionPropertySet? _hoverPosition;
private static readonly string Id = typeof(HoverLight).FullName!;
+ private bool _disposed;
protected override void OnConnected(UIElement targetElement)
{
+#pragma warning disable IDISP001 // Compositor is provided by the system and should not be disposed
Compositor compositor = CompositionTarget.GetCompositorForCurrentThread();
+#pragma warning restore IDISP001
// Create SpotLight and set its properties
- SpotLight spotLight = compositor.CreateSpotLight();
- spotLight.InnerConeAngleInDegrees = 50f;
- spotLight.InnerConeColor = Colors.FloralWhite;
- spotLight.OuterConeAngleInDegrees = 20f;
- spotLight.ConstantAttenuation = 1f;
- spotLight.LinearAttenuation = 0.253f;
- spotLight.QuadraticAttenuation = 0.58f;
+ _spotLight?.Dispose();
+ _spotLight = compositor.CreateSpotLight();
+ _spotLight.InnerConeAngleInDegrees = 50f;
+ _spotLight.InnerConeColor = Colors.FloralWhite;
+ _spotLight.OuterConeAngleInDegrees = 20f;
+ _spotLight.ConstantAttenuation = 1f;
+ _spotLight.LinearAttenuation = 0.253f;
+ _spotLight.QuadraticAttenuation = 0.58f;
// Associate CompositionLight with XamlLight
- CompositionLight = spotLight;
+ CompositionLight = _spotLight;
// Define resting position Animation
Vector3 restingPosition = new(200, 200, 400);
- CubicBezierEasingFunction cbEasing = compositor.CreateCubicBezierEasingFunction(new Vector2(0.3f, 0.7f), new Vector2(0.9f, 0.5f));
- _offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
- _offsetAnimation.InsertKeyFrame(1, restingPosition, cbEasing);
- _offsetAnimation.Duration = TimeSpan.FromSeconds(0.5f);
+ using (CubicBezierEasingFunction cbEasing = compositor.CreateCubicBezierEasingFunction(new Vector2(0.3f, 0.7f), new Vector2(0.9f, 0.5f)))
+ {
+ _offsetAnimation?.Dispose();
+ _offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
+ _offsetAnimation.InsertKeyFrame(1, restingPosition, cbEasing);
+ _offsetAnimation.Duration = TimeSpan.FromSeconds(0.5f);
+ }
- spotLight.Offset = restingPosition;
+ _spotLight.Offset = restingPosition;
// Define expression animation that relates light's offset to pointer position
- CompositionPropertySet hoverPosition = ElementCompositionPreview.GetPointerPositionPropertySet(targetElement);
+ _hoverPosition?.Dispose();
+ _hoverPosition = ElementCompositionPreview.GetPointerPositionPropertySet(targetElement);
+ _lightPositionExpression?.Dispose();
_lightPositionExpression = compositor.CreateExpressionAnimation("Vector3(hover.Position.X, hover.Position.Y, height)");
- _lightPositionExpression.SetReferenceParameter("hover", hoverPosition);
+ _lightPositionExpression.SetReferenceParameter("hover", _hoverPosition);
_lightPositionExpression.SetScalarParameter("height", 100.0f);
// Configure pointer entered/ exited events
@@ -98,14 +109,26 @@ protected override void OnDisconnected(UIElement oldElement)
// Dispose Light and Composition resources when it is removed from the tree
RemoveTargetElement(GetId(), oldElement);
- CompositionLight.Dispose();
-
- _lightPositionExpression?.Dispose();
- _offsetAnimation?.Dispose();
+ Dispose();
}
protected override string GetId()
{
return Id;
}
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _lightPositionExpression?.Dispose();
+ _offsetAnimation?.Dispose();
+ _hoverPosition?.Dispose();
+ _spotLight?.Dispose();
+ CompositionLight = null;
+ _disposed = true;
+ }
}
\ No newline at end of file
diff --git a/AIDevGallery/Controls/Markdown/DefaultSVGRenderer.cs b/AIDevGallery/Controls/Markdown/DefaultSVGRenderer.cs
index 457f73b2..df040c4d 100644
--- a/AIDevGallery/Controls/Markdown/DefaultSVGRenderer.cs
+++ b/AIDevGallery/Controls/Markdown/DefaultSVGRenderer.cs
@@ -13,7 +13,10 @@ internal class DefaultSVGRenderer : ISVGRenderer
{
public async Task SvgToImage(string svgString)
{
+ // SvgImageSource ownership is transferred to Image.Source, so it should not be disposed here
+#pragma warning disable IDISP004 // Don't ignore created IDisposable
SvgImageSource svgImageSource = new SvgImageSource();
+#pragma warning restore IDISP004
var image = new Image();
// Create a MemoryStream object and write the SVG string to it
@@ -27,7 +30,13 @@ public async Task SvgToImage(string svgString)
memoryStream.Position = 0;
// Load the SVG from the MemoryStream
- await svgImageSource.SetSourceAsync(memoryStream.AsRandomAccessStream());
+ // AsRandomAccessStream() returns a wrapper around memoryStream that should not be disposed separately
+ // The wrapper is valid as long as the underlying memoryStream is alive
+ // The await ensures SetSourceAsync completes before memoryStream is disposed at the end of the using block
+#pragma warning disable IDISP001 // Dispose created
+ var randomAccessStream = memoryStream.AsRandomAccessStream();
+#pragma warning restore IDISP001
+ await svgImageSource.SetSourceAsync(randomAccessStream);
}
// Set the Source property of the Image control to the SvgImageSource object
diff --git a/AIDevGallery/Controls/Markdown/TextElements/MyImage.cs b/AIDevGallery/Controls/Markdown/TextElements/MyImage.cs
index 09847a8d..048c2cf7 100644
--- a/AIDevGallery/Controls/Markdown/TextElements/MyImage.cs
+++ b/AIDevGallery/Controls/Markdown/TextElements/MyImage.cs
@@ -17,6 +17,7 @@ namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements;
internal class MyImage : IAddChild
{
+ private static readonly HttpClient _httpClient = new HttpClient();
private InlineUIContainer _container = new InlineUIContainer();
private LinkInline? _linkInline;
private Image _image = new Image();
@@ -108,48 +109,45 @@ private async void LoadImage(object sender, RoutedEventArgs e)
}
else
{
- using (HttpClient client = new())
- {
- // Download data from URL
- HttpResponseMessage response = await client.GetAsync(_uri);
+ // Download data from URL
+ using HttpResponseMessage response = await _httpClient.GetAsync(_uri);
- // Get the Content-Type header
+ // Get the Content-Type header
#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
#pragma warning disable CS8602 // Dereference of a possibly null reference.
- string contentType = response.Content.Headers.ContentType.MediaType;
+ string contentType = response.Content.Headers.ContentType.MediaType;
#pragma warning restore CS8602 // Dereference of a possibly null reference.
#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type.
- if (contentType == "image/svg+xml")
+ if (contentType == "image/svg+xml")
+ {
+ var svgString = await response.Content.ReadAsStringAsync();
+ var resImage = await _svgRenderer.SvgToImage(svgString);
+ if (resImage != null)
{
- var svgString = await response.Content.ReadAsStringAsync();
- var resImage = await _svgRenderer.SvgToImage(svgString);
- if (resImage != null)
- {
- _image = resImage;
- _container.Child = _image;
- }
+ _image = resImage;
+ _container.Child = _image;
}
- else
+ }
+ else
+ {
+ byte[] data = await response.Content.ReadAsByteArrayAsync();
+
+ // Create a BitmapImage for other supported formats
+ BitmapImage bitmap = new BitmapImage();
+ using (InMemoryRandomAccessStream stream = new InMemoryRandomAccessStream())
{
- byte[] data = await response.Content.ReadAsByteArrayAsync();
-
- // Create a BitmapImage for other supported formats
- BitmapImage bitmap = new BitmapImage();
- using (InMemoryRandomAccessStream stream = new InMemoryRandomAccessStream())
- {
- // Write the data to the stream
- await stream.WriteAsync(data.AsBuffer());
- stream.Seek(0);
-
- // Set the source of the BitmapImage
- await bitmap.SetSourceAsync(stream);
- }
-
- _image.Source = bitmap;
- _image.Width = bitmap.PixelWidth == 0 ? bitmap.DecodePixelWidth : bitmap.PixelWidth;
- _image.Height = bitmap.PixelHeight == 0 ? bitmap.DecodePixelHeight : bitmap.PixelHeight;
+ // Write the data to the stream
+ await stream.WriteAsync(data.AsBuffer());
+ stream.Seek(0);
+
+ // Set the source of the BitmapImage
+ await bitmap.SetSourceAsync(stream);
}
+
+ _image.Source = bitmap;
+ _image.Width = bitmap.PixelWidth == 0 ? bitmap.DecodePixelWidth : bitmap.PixelWidth;
+ _image.Height = bitmap.PixelHeight == 0 ? bitmap.DecodePixelHeight : bitmap.PixelHeight;
}
_loaded = true;
diff --git a/AIDevGallery/Controls/ModelPicker/AddHFModelView.xaml.cs b/AIDevGallery/Controls/ModelPicker/AddHFModelView.xaml.cs
index dae5485f..6593b6cb 100644
--- a/AIDevGallery/Controls/ModelPicker/AddHFModelView.xaml.cs
+++ b/AIDevGallery/Controls/ModelPicker/AddHFModelView.xaml.cs
@@ -11,7 +11,6 @@
using Microsoft.UI.Xaml.Controls;
using System;
using System.Collections.ObjectModel;
-using System.Diagnostics;
using System.Linq;
using System.Text.Json;
using System.Threading;
@@ -245,7 +244,10 @@ private async void DownloadModelClicked(object sender, RoutedEventArgs e)
if (output == ContentDialogResult.Primary)
{
+ // ModelDownload lifecycle is managed by ModelDownloadQueue, should not be disposed immediately
+#pragma warning disable IDISP004 // Don't ignore created IDisposable
App.ModelDownloadQueue.AddModel(result!.Details);
+#pragma warning restore IDISP004
result.State = ResultState.Downloading;
}
}
@@ -257,11 +259,7 @@ private void Hyperlink_Click(object sender, RoutedEventArgs e)
string url = result!.License.LicenseUrl ?? $"https://huggingface.co/{result.SearchResult.Id}";
- Process.Start(new ProcessStartInfo()
- {
- FileName = url,
- UseShellExecute = true
- });
+ ProcessHelper.OpenUrl(url);
}
private void ViewModelDetails(object sender, RoutedEventArgs e)
diff --git a/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OllamaPickerView.xaml.cs b/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OllamaPickerView.xaml.cs
index 8e698dcc..81e51f50 100644
--- a/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OllamaPickerView.xaml.cs
+++ b/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OllamaPickerView.xaml.cs
@@ -2,12 +2,12 @@
// Licensed under the MIT License.
using AIDevGallery.ExternalModelUtils;
+using AIDevGallery.Helpers;
using AIDevGallery.Models;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Collections.Generic;
using System.Collections.ObjectModel;
-using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Windows.ApplicationModel.DataTransfer;
@@ -57,11 +57,7 @@ private void OllamaViewModelDetails_Click(object sender, RoutedEventArgs e)
return;
}
- Process.Start(new ProcessStartInfo
- {
- FileName = modelDetailsUrl,
- UseShellExecute = true
- });
+ ProcessHelper.OpenUrl(modelDetailsUrl);
}
}
diff --git a/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OnnxPickerView.xaml.cs b/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OnnxPickerView.xaml.cs
index a2810589..336e5731 100644
--- a/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OnnxPickerView.xaml.cs
+++ b/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OnnxPickerView.xaml.cs
@@ -11,7 +11,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
-using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
@@ -227,7 +226,7 @@ private void OpenModelFolder_Click(object sender, RoutedEventArgs e)
OpenModelFolderEvent.Log(cachedModel.Url);
- Process.Start("explorer.exe", path!);
+ ProcessHelper.OpenFolder(path!);
}
}
}
@@ -302,11 +301,7 @@ private void ViewLicense_Click(object sender, RoutedEventArgs e)
licenseUrl = details.Url;
}
- Process.Start(new ProcessStartInfo()
- {
- FileName = licenseUrl,
- UseShellExecute = true
- });
+ ProcessHelper.OpenUrl(licenseUrl);
}
}
@@ -383,19 +378,11 @@ private void OpenAIToolkitButton_Click(object sender, RoutedEventArgs e)
bool wasDeeplinkSuccessful = true;
try
{
- Process.Start(new ProcessStartInfo()
- {
- FileName = toolkitDeeplink,
- UseShellExecute = true
- });
+ ProcessHelper.OpenUrl(toolkitDeeplink);
}
catch
{
- Process.Start(new ProcessStartInfo()
- {
- FileName = "https://learn.microsoft.com/en-us/windows/ai/toolkit/",
- UseShellExecute = true
- });
+ ProcessHelper.OpenUrl("https://learn.microsoft.com/en-us/windows/ai/toolkit/");
wasDeeplinkSuccessful = false;
}
finally
@@ -406,11 +393,7 @@ private void OpenAIToolkitButton_Click(object sender, RoutedEventArgs e)
private void ViewDocumentationButton_Click(object sender, RoutedEventArgs e)
{
- Process.Start(new ProcessStartInfo()
- {
- FileName = "https://aka.ms/winml-gallery-tutorial",
- UseShellExecute = true
- });
+ ProcessHelper.OpenUrl("https://aka.ms/winml-gallery-tutorial");
}
private async void ShowException(Exception? ex, string? optionalMessage = null)
diff --git a/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OpenAIPickerView.xaml.cs b/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OpenAIPickerView.xaml.cs
index dc96a93f..acba37c0 100644
--- a/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OpenAIPickerView.xaml.cs
+++ b/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OpenAIPickerView.xaml.cs
@@ -2,12 +2,12 @@
// Licensed under the MIT License.
using AIDevGallery.ExternalModelUtils;
+using AIDevGallery.Helpers;
using AIDevGallery.Models;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Collections.Generic;
using System.Collections.ObjectModel;
-using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Windows.ApplicationModel.DataTransfer;
@@ -71,11 +71,7 @@ private void ViewModelDetails_Click(object sender, RoutedEventArgs e)
return;
}
- Process.Start(new ProcessStartInfo
- {
- FileName = modelDetailsUrl,
- UseShellExecute = true
- });
+ ProcessHelper.OpenUrl(modelDetailsUrl);
}
}
diff --git a/AIDevGallery/Controls/ModelSelectionControl.xaml.cs b/AIDevGallery/Controls/ModelSelectionControl.xaml.cs
index c2dac045..fb1f4eee 100644
--- a/AIDevGallery/Controls/ModelSelectionControl.xaml.cs
+++ b/AIDevGallery/Controls/ModelSelectionControl.xaml.cs
@@ -12,7 +12,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
-using System.Diagnostics;
using System.IO;
using System.Linq;
using Windows.ApplicationModel.DataTransfer;
@@ -323,7 +322,7 @@ private void OpenModelFolder_Click(object sender, RoutedEventArgs e)
OpenModelFolderEvent.Log(cachedModel.Url);
- Process.Start("explorer.exe", path!);
+ ProcessHelper.OpenFolder(path!);
}
}
}
@@ -381,11 +380,10 @@ private void ModelCard_Click(object sender, RoutedEventArgs e)
}
}
- Process.Start(new ProcessStartInfo()
+ if (!string.IsNullOrEmpty(modelcardUrl))
{
- FileName = modelcardUrl,
- UseShellExecute = true
- });
+ ProcessHelper.OpenUrl(modelcardUrl);
+ }
}
}
}
@@ -431,11 +429,7 @@ private void ViewLicense_Click(object sender, RoutedEventArgs e)
licenseUrl = details.Url;
}
- Process.Start(new ProcessStartInfo()
- {
- FileName = licenseUrl,
- UseShellExecute = true
- });
+ ProcessHelper.OpenUrl(licenseUrl);
}
}
@@ -605,11 +599,7 @@ private void ViewModelDetails_Click(object sender, RoutedEventArgs e)
return;
}
- Process.Start(new ProcessStartInfo
- {
- FileName = modelDetailsUrl,
- UseShellExecute = true
- });
+ ProcessHelper.OpenUrl(modelDetailsUrl);
}
}
}
\ No newline at end of file
diff --git a/AIDevGallery/Controls/OpacityMask.xaml.cs b/AIDevGallery/Controls/OpacityMask.xaml.cs
index feb92c7e..66c2fb1e 100644
--- a/AIDevGallery/Controls/OpacityMask.xaml.cs
+++ b/AIDevGallery/Controls/OpacityMask.xaml.cs
@@ -6,6 +6,7 @@
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Media;
+using System;
using System.Numerics;
namespace AIDevGallery.Controls;
@@ -16,7 +17,7 @@ namespace AIDevGallery.Controls;
[TemplatePart(Name = RootGridTemplateName, Type = typeof(Grid))]
[TemplatePart(Name = MaskContainerTemplateName, Type = typeof(Border))]
[TemplatePart(Name = ContentPresenterTemplateName, Type = typeof(ContentPresenter))]
-public partial class OpacityMaskView : ContentControl
+public sealed partial class OpacityMaskView : ContentControl, IDisposable
{
// This is from Windows Community Toolkit Labs: https://github.com/CommunityToolkit/Labs-Windows/pull/491
@@ -30,9 +31,14 @@ public partial class OpacityMaskView : ContentControl
private const string MaskContainerTemplateName = "PART_MaskContainer";
private const string RootGridTemplateName = "PART_RootGrid";
- private readonly Compositor _compositor = CompositionTarget.GetCompositorForCurrentThread();
private CompositionBrush? _mask;
private CompositionMaskBrush? _maskBrush;
+ private SpriteVisual? _redirectVisual;
+ private CompositionVisualSurface? _contentVisualSurface;
+ private ExpressionAnimation? _contentSizeAnimation;
+ private CompositionVisualSurface? _maskVisualSurface;
+ private ExpressionAnimation? _maskSizeAnimation;
+ private bool _disposed;
///
/// Initializes a new instance of the class.
@@ -61,28 +67,41 @@ protected override void OnApplyTemplate()
ContentPresenter contentPresenter = (ContentPresenter)GetTemplateChild(ContentPresenterTemplateName);
Border maskContainer = (Border)GetTemplateChild(MaskContainerTemplateName);
- _maskBrush = _compositor.CreateMaskBrush();
- _maskBrush.Source = GetVisualBrush(contentPresenter);
- _mask = GetVisualBrush(maskContainer);
+ // Get compositor locally - it's provided by the system and should not be stored or disposed
+#pragma warning disable IDISP001 // Dispose created - Compositor is a system-managed shared instance
+ Compositor compositor = CompositionTarget.GetCompositorForCurrentThread();
+#pragma warning restore IDISP001
+
+ _maskBrush?.Dispose();
+ _maskBrush = compositor.CreateMaskBrush();
+ _contentVisualSurface?.Dispose();
+ _contentSizeAnimation?.Dispose();
+ _maskBrush.Source = GetVisualBrush(contentPresenter, ref _contentVisualSurface, ref _contentSizeAnimation);
+ _mask?.Dispose();
+ _maskVisualSurface?.Dispose();
+ _maskSizeAnimation?.Dispose();
+ _mask = GetVisualBrush(maskContainer, ref _maskVisualSurface, ref _maskSizeAnimation);
_maskBrush.Mask = OpacityMask is null ? null : _mask;
- SpriteVisual redirectVisual = _compositor.CreateSpriteVisual();
- redirectVisual.RelativeSizeAdjustment = Vector2.One;
- redirectVisual.Brush = _maskBrush;
- ElementCompositionPreview.SetElementChildVisual(rootGrid, redirectVisual);
+ _redirectVisual?.Dispose();
+ _redirectVisual = compositor.CreateSpriteVisual();
+ _redirectVisual.RelativeSizeAdjustment = Vector2.One;
+ _redirectVisual.Brush = _maskBrush;
+ ElementCompositionPreview.SetElementChildVisual(rootGrid, _redirectVisual);
}
- private static CompositionBrush GetVisualBrush(UIElement element)
+ private static CompositionBrush GetVisualBrush(UIElement element, ref CompositionVisualSurface? visualSurface, ref ExpressionAnimation? sizeAnimation)
{
Visual visual = ElementCompositionPreview.GetElementVisual(element);
Compositor compositor = visual.Compositor;
- CompositionVisualSurface visualSurface = compositor.CreateVisualSurface();
+ // Create visual surface and animation
+ visualSurface = compositor.CreateVisualSurface();
visualSurface.SourceVisual = visual;
- ExpressionAnimation sourceSizeAnimation = compositor.CreateExpressionAnimation($"{nameof(visual)}.Size");
- sourceSizeAnimation.SetReferenceParameter(nameof(visual), visual);
- visualSurface.StartAnimation(nameof(visualSurface.SourceSize), sourceSizeAnimation);
+ sizeAnimation = compositor.CreateExpressionAnimation($"{nameof(visual)}.Size");
+ sizeAnimation.SetReferenceParameter(nameof(visual), visual);
+ visualSurface.StartAnimation(nameof(visualSurface.SourceSize), sizeAnimation);
CompositionSurfaceBrush brush = compositor.CreateSurfaceBrush(visualSurface);
@@ -102,4 +121,25 @@ private static void OnOpacityMaskChanged(DependencyObject d, DependencyPropertyC
UIElement? opacityMask = (UIElement?)e.NewValue;
maskBrush.Mask = opacityMask is null ? null : self._mask;
}
+
+ ///
+ /// Disposes the composition resources.
+ ///
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _contentVisualSurface?.Dispose();
+ _contentSizeAnimation?.Dispose();
+ _maskVisualSurface?.Dispose();
+ _maskSizeAnimation?.Dispose();
+ _mask?.Dispose();
+ _maskBrush?.Dispose();
+ _redirectVisual?.Dispose();
+
+ _disposed = true;
+ }
}
\ No newline at end of file
diff --git a/AIDevGallery/Controls/SampleContainer.xaml.cs b/AIDevGallery/Controls/SampleContainer.xaml.cs
index 8c3b955b..ccd2ac21 100644
--- a/AIDevGallery/Controls/SampleContainer.xaml.cs
+++ b/AIDevGallery/Controls/SampleContainer.xaml.cs
@@ -23,7 +23,7 @@
namespace AIDevGallery.Controls;
-internal sealed partial class SampleContainer : UserControl
+internal sealed partial class SampleContainer : UserControl, IDisposable
{
public static readonly DependencyProperty DisclaimerHorizontalAlignmentProperty = DependencyProperty.Register(nameof(DisclaimerHorizontalAlignment), typeof(HorizontalAlignment), typeof(SampleContainer), new PropertyMetadata(defaultValue: HorizontalAlignment.Left));
@@ -68,6 +68,7 @@ public List NugetPackageReferences
private TaskCompletionSource? _sampleLoadedCompletionSource;
private double _codePaneWidth;
private ModelType? _wcrApi;
+ private bool _disposed;
private static readonly List> References = [];
@@ -108,10 +109,16 @@ internal static async Task WaitUnloadAllAsync()
private void CancelCTS()
{
- if (_sampleLoadingCts != null)
+ var cts = _sampleLoadingCts;
+ if (cts == null)
{
- _sampleLoadingCts.Cancel();
- _sampleLoadingCts = null;
+ return;
+ }
+
+ _sampleLoadingCts = null;
+ using (cts)
+ {
+ cts.Cancel();
}
}
@@ -122,7 +129,7 @@ public SampleContainer()
References.Add(new WeakReference(this));
this.Unloaded += (sender, args) =>
{
- CancelCTS();
+ Dispose();
var reference = References.FirstOrDefault(r => r.TryGetTarget(out var sampleContainer) && sampleContainer == this);
if (reference != null)
{
@@ -213,6 +220,7 @@ public async Task LoadSampleAsync(Sample? sample, List? models, Wi
return;
}
+ _sampleLoadingCts?.Dispose();
_sampleLoadingCts = new CancellationTokenSource();
var token = _sampleLoadingCts.Token;
@@ -311,12 +319,10 @@ public async Task LoadSampleAsync(Sample? sample, List? models, Wi
finally
{
_sampleLoadedCompletionSource = null;
+ _sampleLoadingCts?.Dispose();
_sampleLoadingCts = null;
}
- _sampleLoadedCompletionSource = null;
- _sampleLoadingCts = null;
-
NavigatedToSampleLoadedEvent.Log(sample.Name ?? string.Empty);
VisualStateManager.GoToState(this, "SampleLoaded", true);
@@ -587,4 +593,15 @@ private void FooterGrid_SizeChanged(object sender, SizeChangedEventArgs e)
}
}
}
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ CancelCTS();
+ _disposed = true;
+ }
}
\ No newline at end of file
diff --git a/AIDevGallery/Controls/Shimmer.xaml.cs b/AIDevGallery/Controls/Shimmer.xaml.cs
index 2ec342b8..86a38392 100644
--- a/AIDevGallery/Controls/Shimmer.xaml.cs
+++ b/AIDevGallery/Controls/Shimmer.xaml.cs
@@ -19,7 +19,7 @@ namespace AIDevGallery.Controls;
/// A generic shimmer control that can be used to construct a beautiful loading effect.
///
[TemplatePart(Name = PART_Shape, Type = typeof(Rectangle))]
-internal partial class Shimmer : Control
+internal sealed partial class Shimmer : Control, IDisposable
{
///
/// Identifies the dependency property.
@@ -68,9 +68,11 @@ public bool IsActive
private ShapeVisual? _shapeVisual;
private CompositionLinearGradientBrush? _shimmerMaskGradient;
private Border? _shape;
+ private CompositionSpriteShape? _spriteShape;
private bool _initialized;
private bool _animationStarted;
+ private bool _disposed;
public Shimmer()
{
@@ -103,22 +105,6 @@ private void OnLoaded(object sender, RoutedEventArgs e)
private void OnUnloaded(object sender, RoutedEventArgs e)
{
ActualThemeChanged -= OnActualThemeChanged;
- StopAnimation();
-
- if (_initialized && _shape != null)
- {
- ElementCompositionPreview.SetElementChildVisual(_shape, null);
-
- _rectangleGeometry!.Dispose();
- _shapeVisual!.Dispose();
- _shimmerMaskGradient!.Dispose();
- _gradientStop1!.Dispose();
- _gradientStop2!.Dispose();
- _gradientStop3!.Dispose();
- _gradientStop4!.Dispose();
-
- _initialized = false;
- }
}
private void OnActualThemeChanged(FrameworkElement sender, object args)
@@ -143,21 +129,39 @@ private bool TryInitializationResource()
return false;
}
- var compositor = _shape.GetVisual().Compositor;
+#pragma warning disable IDISP001 // shapeVisual and compositor are provided by the system
+ var shapeVisual = _shape.GetVisual();
+ var compositor = shapeVisual.Compositor;
+#pragma warning restore IDISP001
+ _rectangleGeometry?.Dispose();
_rectangleGeometry = compositor.CreateRoundedRectangleGeometry();
+ _shapeVisual?.Dispose();
_shapeVisual = compositor.CreateShapeVisual();
+ _shimmerMaskGradient?.Dispose();
_shimmerMaskGradient = compositor.CreateLinearGradientBrush();
+ _gradientStop1?.Dispose();
_gradientStop1 = compositor.CreateColorGradientStop();
+ _gradientStop2?.Dispose();
_gradientStop2 = compositor.CreateColorGradientStop();
+ _gradientStop3?.Dispose();
_gradientStop3 = compositor.CreateColorGradientStop();
+ _gradientStop4?.Dispose();
_gradientStop4 = compositor.CreateColorGradientStop();
SetGradientAndStops();
SetGradientStopColorsByTheme();
_rectangleGeometry.CornerRadius = new Vector2((float)CornerRadius.TopLeft);
- var spriteShape = compositor.CreateSpriteShape(_rectangleGeometry);
- spriteShape.FillBrush = _shimmerMaskGradient;
- _shapeVisual.Shapes.Add(spriteShape);
+
+ // Clear and dispose old shapes before adding new one
+ if (_shapeVisual.Shapes.Count > 0)
+ {
+ _spriteShape?.Dispose();
+ _shapeVisual.Shapes.Clear();
+ }
+
+ _spriteShape = compositor.CreateSpriteShape(_rectangleGeometry);
+ _spriteShape.FillBrush = _shimmerMaskGradient;
+ _shapeVisual.Shapes.Add(_spriteShape);
ElementCompositionPreview.SetElementChildVisual(_shape, _shapeVisual);
_initialized = true;
@@ -207,11 +211,15 @@ private void TryStartAnimation()
return;
}
+#pragma warning disable IDISP001, IDISP004 // rootVisual and its reference are provided by the system
var rootVisual = _shape.GetVisual();
+ _sizeAnimation?.Dispose();
_sizeAnimation = rootVisual.GetReference().Size;
+#pragma warning restore IDISP001, IDISP004
_shapeVisual.StartAnimation(nameof(ShapeVisual.Size), _sizeAnimation);
_rectangleGeometry.StartAnimation(nameof(CompositionRoundedRectangleGeometry.Size), _sizeAnimation);
+ _gradientStartPointAnimation?.Dispose();
_gradientStartPointAnimation = rootVisual.Compositor.CreateVector2KeyFrameAnimation();
_gradientStartPointAnimation.Duration = Duration;
_gradientStartPointAnimation.IterationBehavior = AnimationIterationBehavior.Forever;
@@ -219,6 +227,7 @@ private void TryStartAnimation()
_gradientStartPointAnimation.InsertKeyFrame(1.0f, Vector2.Zero);
_shimmerMaskGradient!.StartAnimation(nameof(CompositionLinearGradientBrush.StartPoint), _gradientStartPointAnimation);
+ _gradientEndPointAnimation?.Dispose();
_gradientEndPointAnimation = rootVisual.Compositor.CreateVector2KeyFrameAnimation();
_gradientEndPointAnimation.Duration = Duration;
_gradientEndPointAnimation.IterationBehavior = AnimationIterationBehavior.Forever;
@@ -240,10 +249,6 @@ private void StopAnimation()
_rectangleGeometry!.StopAnimation(nameof(CompositionRoundedRectangleGeometry.Size));
_shimmerMaskGradient!.StopAnimation(nameof(CompositionLinearGradientBrush.StartPoint));
_shimmerMaskGradient.StopAnimation(nameof(CompositionLinearGradientBrush.EndPoint));
-
- _sizeAnimation!.Dispose();
- _gradientStartPointAnimation!.Dispose();
- _gradientEndPointAnimation!.Dispose();
_animationStarted = false;
}
@@ -260,4 +265,35 @@ private static void PropertyChanged(DependencyObject s, DependencyPropertyChange
self.StopAnimation();
}
}
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ StopAnimation();
+
+ if (_initialized && _shape != null)
+ {
+ ElementCompositionPreview.SetElementChildVisual(_shape, null);
+
+ _rectangleGeometry?.Dispose();
+ _shapeVisual?.Dispose();
+ _shimmerMaskGradient?.Dispose();
+ _gradientStop1?.Dispose();
+ _gradientStop2?.Dispose();
+ _gradientStop3?.Dispose();
+ _gradientStop4?.Dispose();
+ _spriteShape?.Dispose();
+ _gradientStartPointAnimation?.Dispose();
+ _gradientEndPointAnimation?.Dispose();
+ _sizeAnimation?.Dispose();
+
+ _initialized = false;
+ }
+
+ _disposed = true;
+ }
}
\ No newline at end of file
diff --git a/AIDevGallery/ExternalModelUtils/FoundryLocal/FoundryClient.cs b/AIDevGallery/ExternalModelUtils/FoundryLocal/FoundryClient.cs
index 107c6925..6455ce59 100644
--- a/AIDevGallery/ExternalModelUtils/FoundryLocal/FoundryClient.cs
+++ b/AIDevGallery/ExternalModelUtils/FoundryLocal/FoundryClient.cs
@@ -11,7 +11,7 @@
namespace AIDevGallery.ExternalModelUtils.FoundryLocal;
-internal class FoundryClient : IDisposable
+internal sealed class FoundryClient : IDisposable
{
private readonly Dictionary _loadedModels = new();
private readonly Dictionary _modelMaxOutputTokens = new();
diff --git a/AIDevGallery/ExternalModelUtils/FoundryLocal/FoundryLocalChatClientAdapter.cs b/AIDevGallery/ExternalModelUtils/FoundryLocal/FoundryLocalChatClientAdapter.cs
index 72a07c16..c785cfb7 100644
--- a/AIDevGallery/ExternalModelUtils/FoundryLocal/FoundryLocalChatClientAdapter.cs
+++ b/AIDevGallery/ExternalModelUtils/FoundryLocal/FoundryLocalChatClientAdapter.cs
@@ -15,7 +15,7 @@ namespace AIDevGallery.ExternalModelUtils.FoundryLocal;
/// Adapter that wraps FoundryLocal SDK's native OpenAIChatClient to work with Microsoft.Extensions.AI.IChatClient.
/// Uses the SDK's direct model API (no web service) to avoid SSE compatibility issues.
///
-internal class FoundryLocalChatClientAdapter : IChatClient
+internal sealed class FoundryLocalChatClientAdapter : IChatClient
{
private const int DefaultMaxTokens = 1024;
diff --git a/AIDevGallery/ExternalModelUtils/FoundryLocalModelProvider.cs b/AIDevGallery/ExternalModelUtils/FoundryLocalModelProvider.cs
index c81150c7..bc65bd5f 100644
--- a/AIDevGallery/ExternalModelUtils/FoundryLocalModelProvider.cs
+++ b/AIDevGallery/ExternalModelUtils/FoundryLocalModelProvider.cs
@@ -14,11 +14,12 @@
namespace AIDevGallery.ExternalModelUtils;
-internal class FoundryLocalModelProvider : IExternalModelProvider
+internal sealed class FoundryLocalModelProvider : IExternalModelProvider, IDisposable
{
private IEnumerable? _downloadedModels;
private IEnumerable? _catalogModels;
private FoundryClient? _foundryManager;
+ private bool _disposed;
public static FoundryLocalModelProvider Instance { get; } = new FoundryLocalModelProvider();
@@ -285,6 +286,7 @@ public async Task RetryInitializationAsync()
{
_downloadedModels = null;
_catalogModels = null;
+ _foundryManager?.Dispose();
_foundryManager = null;
await InitializeAsync();
@@ -299,7 +301,10 @@ private async Task InitializeAsync(CancellationToken cancellationToken = default
return;
}
- _foundryManager = _foundryManager ?? await FoundryClient.CreateAsync();
+ if (_foundryManager == null)
+ {
+ _foundryManager = await FoundryClient.CreateAsync();
+ }
if (_foundryManager == null)
{
@@ -499,4 +504,15 @@ public async Task ClearAllCacheAsync()
return false;
}
}
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _foundryManager?.Dispose();
+ _disposed = true;
+ }
}
\ No newline at end of file
diff --git a/AIDevGallery/Helpers/ProcessHelper.cs b/AIDevGallery/Helpers/ProcessHelper.cs
new file mode 100644
index 00000000..ea8b8244
--- /dev/null
+++ b/AIDevGallery/Helpers/ProcessHelper.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Diagnostics;
+
+namespace AIDevGallery.Helpers;
+
+///
+/// Helper methods for starting external processes (browser, explorer, etc.)
+/// where the process lifecycle is independent of the application.
+///
+internal static class ProcessHelper
+{
+ ///
+ /// Starts a process using the shell to open a URL in the default browser.
+ ///
+ /// The URL to open.
+ public static void OpenUrl(string url)
+ {
+ if (!string.IsNullOrWhiteSpace(url))
+ {
+ // Process.Start for external process (browser) doesn't need disposal - process lifecycle is independent
+#pragma warning disable IDISP004 // Don't ignore created IDisposable
+ Process.Start(new ProcessStartInfo()
+ {
+ FileName = url,
+ UseShellExecute = true
+ });
+#pragma warning restore IDISP004
+ }
+ }
+
+ ///
+ /// Opens Windows Explorer to the specified folder path.
+ ///
+ /// The folder path to open in Explorer.
+ public static void OpenFolder(string folderPath)
+ {
+ if (!string.IsNullOrWhiteSpace(folderPath))
+ {
+ // Process.Start for explorer doesn't need disposal - process lifecycle is independent
+#pragma warning disable IDISP004 // Don't ignore created IDisposable
+ Process.Start("explorer.exe", folderPath);
+#pragma warning restore IDISP004
+ }
+ }
+}
\ No newline at end of file
diff --git a/AIDevGallery/Pages/APIs/APIPage.xaml.cs b/AIDevGallery/Pages/APIs/APIPage.xaml.cs
index acd20914..c4963335 100644
--- a/AIDevGallery/Pages/APIs/APIPage.xaml.cs
+++ b/AIDevGallery/Pages/APIs/APIPage.xaml.cs
@@ -12,7 +12,6 @@
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
using System;
-using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Windows.ApplicationModel.DataTransfer;
@@ -184,11 +183,8 @@ private void MarkdownTextBlock_OnLinkClicked(object sender, CommunityToolkit.Lab
}
ModelDetailsLinkClickedEvent.Log(link);
- Process.Start(new ProcessStartInfo()
- {
- FileName = link,
- UseShellExecute = true
- });
+
+ ProcessHelper.OpenUrl(link);
}
private void SampleList_ItemInvoked(ItemsView sender, ItemsViewItemInvokedEventArgs args)
diff --git a/AIDevGallery/Pages/Models/ModelPage.xaml.cs b/AIDevGallery/Pages/Models/ModelPage.xaml.cs
index 6cc65b69..220215c4 100644
--- a/AIDevGallery/Pages/Models/ModelPage.xaml.cs
+++ b/AIDevGallery/Pages/Models/ModelPage.xaml.cs
@@ -254,21 +254,13 @@ private void ToolkitActionFlyoutItem_Click(object sender, RoutedEventArgs e)
bool wasDeeplinkSuccessful = true;
try
{
- Process.Start(new ProcessStartInfo()
- {
- FileName = toolkitDeeplink,
- UseShellExecute = true
- });
+ ProcessHelper.OpenUrl(toolkitDeeplink);
}
catch
{
try
{
- Process.Start(new ProcessStartInfo()
- {
- FileName = "https://learn.microsoft.com/en-us/windows/ai/toolkit/",
- UseShellExecute = true
- });
+ ProcessHelper.OpenUrl("https://learn.microsoft.com/en-us/windows/ai/toolkit/");
}
catch (Exception ex)
{
@@ -328,12 +320,7 @@ private void MarkdownTextBlock_OnLinkClicked(object sender, CommunityToolkit.Lab
{
try
{
- var psi = new ProcessStartInfo
- {
- FileName = uri.AbsoluteUri,
- UseShellExecute = true
- };
- Process.Start(psi);
+ ProcessHelper.OpenUrl(uri.AbsoluteUri);
}
catch (Exception ex) when (ex is Win32Exception
|| ex is InvalidOperationException
diff --git a/AIDevGallery/Pages/SettingsPage.xaml.cs b/AIDevGallery/Pages/SettingsPage.xaml.cs
index 4c530e9c..0455ffa6 100644
--- a/AIDevGallery/Pages/SettingsPage.xaml.cs
+++ b/AIDevGallery/Pages/SettingsPage.xaml.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
+using AIDevGallery.Helpers;
using AIDevGallery.Models;
using AIDevGallery.Telemetry.Events;
using AIDevGallery.Utils;
@@ -10,7 +11,6 @@
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.ObjectModel;
-using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
@@ -95,7 +95,7 @@ private void FolderPathTxt_Click(object sender, RoutedEventArgs e)
{
if (cacheFolderPath != null)
{
- Process.Start("explorer.exe", cacheFolderPath);
+ ProcessHelper.OpenFolder(cacheFolderPath);
}
}
@@ -201,7 +201,7 @@ private void ModelFolder_Click(object sender, RoutedEventArgs e)
if (path != null && Directory.Exists(path))
{
- Process.Start("explorer.exe", path);
+ ProcessHelper.OpenFolder(path);
}
}
}
diff --git a/AIDevGallery/Program.cs b/AIDevGallery/Program.cs
index dc395e49..eceffc00 100644
--- a/AIDevGallery/Program.cs
+++ b/AIDevGallery/Program.cs
@@ -73,14 +73,15 @@ private static bool DecideRedirection()
// wait method to wait for the redirection to complete.
private static void RedirectActivationTo(AppActivationArguments args, AppInstance keyInstance)
{
- var redirectSemaphore = new Semaphore(0, 1);
- Task.Run(() =>
+ using (var redirectSemaphore = new Semaphore(0, 1))
+ {
+ Task.Run(() =>
{
keyInstance.RedirectActivationToAsync(args).AsTask().Wait();
redirectSemaphore.Release();
});
- redirectSemaphore.WaitOne();
- redirectSemaphore.Dispose();
+ redirectSemaphore.WaitOne();
+ }
// Bring the window to the foreground
Process process = Process.GetProcessById((int)keyInstance.ProcessId);
diff --git a/AIDevGallery/Samples/Open Source Models/Embeddings/RetrievalAugmentedGeneration.xaml.cs b/AIDevGallery/Samples/Open Source Models/Embeddings/RetrievalAugmentedGeneration.xaml.cs
index 0fe9891d..9430a4c9 100644
--- a/AIDevGallery/Samples/Open Source Models/Embeddings/RetrievalAugmentedGeneration.xaml.cs
+++ b/AIDevGallery/Samples/Open Source Models/Embeddings/RetrievalAugmentedGeneration.xaml.cs
@@ -47,7 +47,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.SentenceEmbeddings.Embeddings;
],
Id = "9C1FB14D-4841-449C-9563-4551106BB693",
Icon = "\uE8D4")]
-internal sealed partial class RetrievalAugmentedGeneration : BaseSamplePage
+internal sealed partial class RetrievalAugmentedGeneration : BaseSamplePage, IDisposable
{
private EmbeddingGenerator? _embeddings;
private IChatClient? _chatClient;
@@ -58,6 +58,7 @@ internal sealed partial class RetrievalAugmentedGeneration : BaseSamplePage
private CancellationTokenSource? _cts;
private bool _isCancellable;
private bool isImeActive = true;
+ private bool _disposed;
private List? selectedPages;
private int selectedPageIndex = -1;
@@ -91,7 +92,9 @@ protected override async Task LoadModelAsync(MultiModelSampleNavigationParameter
bool compileModel = sampleParams.WinMlSampleOptions.CompileModel;
string? deviceType = sampleParams.WinMlSampleOptions.DeviceType;
+ _embeddings?.Dispose();
_embeddings = await EmbeddingGenerator.CreateAsync(modelPath, policy, epName, compileModel, deviceType);
+ (_chatClient as IDisposable)?.Dispose();
_chatClient = await sampleParams.GetIChatClientAsync();
}
catch (Exception ex)
@@ -356,11 +359,13 @@ private async Task UpdatePdfImageAsync()
return;
}
- var page = pdfDocument.GetPage(pageId - 1);
- _inMemoryRandomAccessStream?.Dispose();
- _inMemoryRandomAccessStream = new();
- var rect = page.Dimensions.TrimBox;
- await page.RenderToStreamAsync(_inMemoryRandomAccessStream).AsTask().ConfigureAwait(false);
+ using (var page = pdfDocument.GetPage(pageId - 1))
+ {
+ _inMemoryRandomAccessStream?.Dispose();
+ _inMemoryRandomAccessStream = new();
+ var rect = page.Dimensions.TrimBox;
+ await page.RenderToStreamAsync(_inMemoryRandomAccessStream).AsTask().ConfigureAwait(false);
+ }
DispatcherQueue.TryEnqueue(() =>
{
@@ -569,4 +574,20 @@ private void HideProgress()
IndexingProgressBar.Value = 0;
ProgressStatusTextBlock.Text = string.Empty;
}
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _embeddings?.Dispose();
+ (_chatClient as IDisposable)?.Dispose();
+ _inMemoryRandomAccessStream?.Dispose();
+ _cts?.Dispose();
+ (_pdfPages as IDisposable)?.Dispose();
+ (_vectorStore as IDisposable)?.Dispose();
+ _disposed = true;
+ }
}
\ No newline at end of file
diff --git a/AIDevGallery/Samples/Open Source Models/Embeddings/SemanticSearch.xaml.cs b/AIDevGallery/Samples/Open Source Models/Embeddings/SemanticSearch.xaml.cs
index 0510bb43..00c9a3d9 100644
--- a/AIDevGallery/Samples/Open Source Models/Embeddings/SemanticSearch.xaml.cs
+++ b/AIDevGallery/Samples/Open Source Models/Embeddings/SemanticSearch.xaml.cs
@@ -40,7 +40,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.SentenceEmbeddings.Embeddings;
],
Id = "41391b3f-f143-4719-a171-b0ce9c4cdcd6",
Icon = "\uE8D4")]
-internal sealed partial class SemanticSearch : BaseSamplePage
+internal sealed partial class SemanticSearch : BaseSamplePage, IDisposable
{
private EmbeddingGenerator? _embeddings;
private CancellationTokenSource cts = new();
@@ -66,6 +66,7 @@ protected async override Task LoadModelAsync(SampleNavigationParameters samplePa
bool compileModel = sampleParams.WinMlSampleOptions.CompileModel;
string? deviceType = sampleParams.WinMlSampleOptions.DeviceType;
+ _embeddings?.Dispose();
_embeddings = await EmbeddingGenerator.CreateAsync(modelPath, policy, epName, compileModel, deviceType);
sampleParams.NotifyCompletion();
}
@@ -93,6 +94,12 @@ protected override void OnNavigatedFrom(NavigationEventArgs e)
private void CleanUp()
{
_embeddings?.Dispose();
+ cts?.Dispose();
+ }
+
+ public void Dispose()
+ {
+ CleanUp();
}
private void SemanticTextBox_TextChanged(object sender, TextChangedEventArgs e)
@@ -145,8 +152,8 @@ public void Search(string sourceText, string searchText)
Task.Run(
async () =>
{
- VectorStore? vectorStore = new InMemoryVectorStore();
- VectorStoreCollection