From 9c908a34d704afee7eaf4a2430ceb0e2a2a3a299 Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Mon, 5 Jan 2026 17:27:46 +0800 Subject: [PATCH 01/23] fix --- AIDevGallery/AIDevGallery.csproj | 2 - .../Controls/DownloadProgressList.xaml.cs | 8 +- .../HomePage/Header/Lights/AmbLight.cs | 9 ++ .../HomePage/Header/Lights/HoverLight.cs | 71 ++++++--- .../Controls/Markdown/DefaultSVGRenderer.cs | 7 +- .../Controls/Markdown/TextElements/MyImage.cs | 63 ++++---- .../ModelPicker/AddHFModelView.xaml.cs | 12 +- .../ModelPickerViews/OllamaPickerView.xaml.cs | 6 +- .../ModelPickerViews/OnnxPickerView.xaml.cs | 30 ++-- .../ModelPickerViews/OpenAIPickerView.xaml.cs | 6 +- .../Controls/ModelSelectionControl.xaml.cs | 22 ++- AIDevGallery/Controls/OpacityMask.xaml.cs | 37 ++++- AIDevGallery/Controls/SampleContainer.xaml.cs | 11 +- AIDevGallery/Controls/Shimmer.xaml.cs | 138 +++++++++++------- .../FoundryLocal/FoundryClient.cs | 2 +- .../FoundryLocalChatClientAdapter.cs | 2 +- .../FoundryLocalModelProvider.cs | 20 ++- AIDevGallery/Pages/APIs/APIPage.xaml.cs | 6 +- AIDevGallery/Pages/Models/ModelPage.xaml.cs | 18 ++- AIDevGallery/Pages/SettingsPage.xaml.cs | 8 +- AIDevGallery/Program.cs | 9 +- .../RetrievalAugmentedGeneration.xaml.cs | 33 ++++- .../Embeddings/SemanticSearch.xaml.cs | 13 +- .../ESRGAN/SuperResolution.xaml.cs | 10 +- .../Image Models/FFNet/SegmentStreets.xaml.cs | 10 +- .../FaceDetLite/FaceDetection.xaml.cs | 12 +- .../Faster RCNN/ObjectDetection.xaml.cs | 10 +- .../HRNetPose/PoseDetection.xaml.cs | 10 +- .../ImageNet/ImageClassification.xaml.cs | 10 +- .../MultiHRNetPose/Multipose.xaml.cs | 2 +- .../SINet/DetectBackground.xaml.cs | 71 +++++---- .../YOLOv4/YOLOObjectionDetection.xaml.cs | 116 ++++++++------- .../Language Models/Chat.xaml.cs | 9 +- .../Language Models/ContentModeration.xaml.cs | 9 +- .../CustomSystemPrompt.xaml.cs | 9 +- .../Language Models/ExplainCode.xaml.cs | 9 +- .../Language Models/Generate.xaml.cs | 9 +- .../Language Models/GenerateCode.xaml.cs | 9 +- .../Language Models/GrammarCheck.xaml.cs | 9 +- .../Language Models/Paraphrase.xaml.cs | 9 +- .../SemanticKernelChat.xaml.cs | 6 +- .../Language Models/SentimentAnalysis.xaml.cs | 9 +- .../Language Models/SmartPaste.xaml.cs | 8 +- .../Language Models/SmartText.xaml.cs | 8 +- .../Language Models/Summarize.xaml.cs | 10 +- .../Language Models/Translate.xaml.cs | 9 +- .../Multimodal Models/DescribeImage.xaml.cs | 9 +- .../Stable Diffusion/GenerateImage.xaml.cs | 8 +- .../Whisper/WhisperAudioTranscription.xaml.cs | 8 +- .../Whisper/WhisperAudioTranslation.xaml.cs | 8 +- .../Whisper/WhisperLiveTranscription.xaml.cs | 8 +- .../Samples/SharedCode/AudioRecorder.cs | 2 +- .../Samples/SharedCode/BitmapFunctions.cs | 32 ++-- .../SharedCode/Controls/SmartPasteForm.cs | 40 ++--- .../Embeddings/EmbeddingGenerator.cs | 2 +- .../OnnxRuntimeGenAIChatClientFactory.cs | 3 +- .../SharedCode/IChatClient/PhiSilicaClient.cs | 28 +++- .../StableDiffusionCode/SafetyChecker.cs | 2 +- .../StableDiffusionCode/StableDiffusion.cs | 6 +- .../StableDiffusionCode/TextProcessing.cs | 2 +- .../StableDiffusionCode/VaeDecoder.cs | 2 +- .../Samples/SharedCode/WhisperWrapper.cs | 4 +- .../Samples/WCRAPIs/BackgroundRemover.xaml.cs | 6 +- .../Samples/WCRAPIs/ColoringBook.xaml.cs | 41 ++++-- .../WCRAPIs/DescribeYourChange.xaml.cs | 8 +- .../WCRAPIs/ForegroundExtractor.xaml.cs | 17 ++- .../Samples/WCRAPIs/ImageDescription.xaml.cs | 9 +- .../Samples/WCRAPIs/IncreaseFidelity.xaml.cs | 5 +- .../WCRAPIs/KnowledgeRetrieval.xaml.cs | 11 +- .../Samples/WCRAPIs/MagicEraser.xaml.cs | 3 + .../Samples/WCRAPIs/PhiSilicaBasic.xaml.cs | 8 +- .../Samples/WCRAPIs/PhiSilicaLoRa.xaml.cs | 13 +- .../Samples/WCRAPIs/RestyleImage.xaml.cs | 12 +- AIDevGallery/Samples/WCRAPIs/SDXL.xaml.cs | 12 +- .../Samples/WCRAPIs/TextRewrite.xaml.cs | 8 +- .../Samples/WCRAPIs/TextSummarize.xaml.cs | 8 +- .../Samples/WCRAPIs/TextToTable.xaml.cs | 8 +- AIDevGallery/Utils/GithubApi.cs | 4 +- AIDevGallery/Utils/HuggingFaceApi.cs | 10 +- AIDevGallery/Utils/ModelDownload.cs | 15 +- AIDevGallery/Utils/ModelDownloadQueue.cs | 1 - AIDevGallery/ViewModels/DownloadableModel.cs | 2 + 82 files changed, 884 insertions(+), 387 deletions(-) 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..10e07af9 100644 --- a/AIDevGallery/Controls/DownloadProgressList.xaml.cs +++ b/AIDevGallery/Controls/DownloadProgressList.xaml.cs @@ -72,7 +72,9 @@ private void RetryDownloadClicked(object sender, RoutedEventArgs e) if (sender is Button button && button.Tag is DownloadableModel downloadableModel) { downloadProgresses.Remove(downloadableModel); - App.ModelDownloadQueue.AddModel(downloadableModel.ModelDetails); + using (App.ModelDownloadQueue.AddModel(downloadableModel.ModelDetails)) + { + } } } @@ -93,7 +95,9 @@ private void VerificationFailedClicked(object sender, RoutedEventArgs e) if (sender is Button button && button.Tag is DownloadableModel downloadableModel) { downloadProgresses.Remove(downloadableModel); - App.ModelDownloadQueue.AddModel(downloadableModel.ModelDetails); + using (App.ModelDownloadQueue.AddModel(downloadableModel.ModelDetails)) + { + } } } } \ 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..801dec22 100644 --- a/AIDevGallery/Controls/HomePage/Header/Lights/HoverLight.cs +++ b/AIDevGallery/Controls/HomePage/Header/Lights/HoverLight.cs @@ -11,41 +11,55 @@ 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 Compositor? _compositor; + private CompositionPropertySet? _hoverPosition; private static readonly string Id = typeof(HoverLight).FullName!; + private bool _disposed; protected override void OnConnected(UIElement targetElement) { - Compositor compositor = CompositionTarget.GetCompositorForCurrentThread(); + _compositor?.Dispose(); + _compositor = CompositionTarget.GetCompositorForCurrentThread(); // 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; + + // Dispose previous CompositionLight if exists + CompositionLight?.Dispose(); // 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); - _lightPositionExpression = compositor.CreateExpressionAnimation("Vector3(hover.Position.X, hover.Position.Y, height)"); - _lightPositionExpression.SetReferenceParameter("hover", hoverPosition); + _hoverPosition?.Dispose(); + _hoverPosition = ElementCompositionPreview.GetPointerPositionPropertySet(targetElement); + _lightPositionExpression?.Dispose(); + _lightPositionExpression = _compositor.CreateExpressionAnimation("Vector3(hover.Position.X, hover.Position.Y, height)"); + _lightPositionExpression.SetReferenceParameter("hover", _hoverPosition); _lightPositionExpression.SetScalarParameter("height", 100.0f); // Configure pointer entered/ exited events @@ -98,14 +112,27 @@ 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; + } + + CompositionLight?.Dispose(); + _lightPositionExpression?.Dispose(); + _offsetAnimation?.Dispose(); + _spotLight?.Dispose(); + _compositor?.Dispose(); + _hoverPosition?.Dispose(); + _disposed = true; + } } \ No newline at end of file diff --git a/AIDevGallery/Controls/Markdown/DefaultSVGRenderer.cs b/AIDevGallery/Controls/Markdown/DefaultSVGRenderer.cs index 457f73b2..84c4d8d9 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,9 @@ public async Task SvgToImage(string svgString) memoryStream.Position = 0; // Load the SVG from the MemoryStream - await svgImageSource.SetSourceAsync(memoryStream.AsRandomAccessStream()); + // AsRandomAccessStream() returns an IDisposable that's owned by the SvgImageSource after SetSourceAsync + using var randomAccessStream = memoryStream.AsRandomAccessStream(); + 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..954201c0 100644 --- a/AIDevGallery/Controls/Markdown/TextElements/MyImage.cs +++ b/AIDevGallery/Controls/Markdown/TextElements/MyImage.cs @@ -108,48 +108,47 @@ private async void LoadImage(object sender, RoutedEventArgs e) } else { - using (HttpClient client = new()) - { - // Download data from URL - HttpResponseMessage response = await client.GetAsync(_uri); + using HttpClient client = new(); + + // Download data from URL + using HttpResponseMessage response = await client.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 8fad62ef..951f0667 100644 --- a/AIDevGallery/Controls/ModelPicker/AddHFModelView.xaml.cs +++ b/AIDevGallery/Controls/ModelPicker/AddHFModelView.xaml.cs @@ -245,8 +245,10 @@ private async void DownloadModelClicked(object sender, RoutedEventArgs e) if (output == ContentDialogResult.Primary) { - App.ModelDownloadQueue.AddModel(result!.Details); - result.State = ResultState.Downloading; + using (App.ModelDownloadQueue.AddModel(result!.Details)) + { + result.State = ResultState.Downloading; + } } } @@ -257,11 +259,13 @@ private void Hyperlink_Click(object sender, RoutedEventArgs e) string url = result!.License.LicenseUrl ?? $"https://huggingface.co/{result.SearchResult.Id}"; - Process.Start(new ProcessStartInfo() + using (Process.Start(new ProcessStartInfo() { FileName = url, UseShellExecute = true - }); + })) + { + } } 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..924c0c3b 100644 --- a/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OllamaPickerView.xaml.cs +++ b/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OllamaPickerView.xaml.cs @@ -57,11 +57,13 @@ private void OllamaViewModelDetails_Click(object sender, RoutedEventArgs e) return; } - Process.Start(new ProcessStartInfo + using (Process.Start(new ProcessStartInfo { FileName = modelDetailsUrl, UseShellExecute = true - }); + })) + { + } } } diff --git a/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OnnxPickerView.xaml.cs b/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OnnxPickerView.xaml.cs index a2810589..1e3a1f66 100644 --- a/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OnnxPickerView.xaml.cs +++ b/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OnnxPickerView.xaml.cs @@ -227,7 +227,9 @@ private void OpenModelFolder_Click(object sender, RoutedEventArgs e) OpenModelFolderEvent.Log(cachedModel.Url); - Process.Start("explorer.exe", path!); + using (Process.Start("explorer.exe", path!)) + { + } } } } @@ -302,11 +304,13 @@ private void ViewLicense_Click(object sender, RoutedEventArgs e) licenseUrl = details.Url; } - Process.Start(new ProcessStartInfo() + using (Process.Start(new ProcessStartInfo() { FileName = licenseUrl, UseShellExecute = true - }); + })) + { + } } } @@ -383,20 +387,24 @@ private void OpenAIToolkitButton_Click(object sender, RoutedEventArgs e) bool wasDeeplinkSuccessful = true; try { - Process.Start(new ProcessStartInfo() + using (Process.Start(new ProcessStartInfo() { FileName = toolkitDeeplink, UseShellExecute = true - }); + })) + { + } } catch { - Process.Start(new ProcessStartInfo() + using (Process.Start(new ProcessStartInfo() { FileName = "https://learn.microsoft.com/en-us/windows/ai/toolkit/", UseShellExecute = true - }); - wasDeeplinkSuccessful = false; + })) + { + wasDeeplinkSuccessful = false; + } } finally { @@ -406,11 +414,13 @@ private void OpenAIToolkitButton_Click(object sender, RoutedEventArgs e) private void ViewDocumentationButton_Click(object sender, RoutedEventArgs e) { - Process.Start(new ProcessStartInfo() + using (Process.Start(new ProcessStartInfo() { FileName = "https://aka.ms/winml-gallery-tutorial", UseShellExecute = true - }); + })) + { + } } 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..385c8d18 100644 --- a/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OpenAIPickerView.xaml.cs +++ b/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OpenAIPickerView.xaml.cs @@ -71,11 +71,13 @@ private void ViewModelDetails_Click(object sender, RoutedEventArgs e) return; } - Process.Start(new ProcessStartInfo + using (Process.Start(new ProcessStartInfo { FileName = modelDetailsUrl, UseShellExecute = true - }); + })) + { + } } } diff --git a/AIDevGallery/Controls/ModelSelectionControl.xaml.cs b/AIDevGallery/Controls/ModelSelectionControl.xaml.cs index c2dac045..7730ce4b 100644 --- a/AIDevGallery/Controls/ModelSelectionControl.xaml.cs +++ b/AIDevGallery/Controls/ModelSelectionControl.xaml.cs @@ -323,7 +323,9 @@ private void OpenModelFolder_Click(object sender, RoutedEventArgs e) OpenModelFolderEvent.Log(cachedModel.Url); - Process.Start("explorer.exe", path!); + using (Process.Start("explorer.exe", path!)) + { + } } } } @@ -381,11 +383,13 @@ private void ModelCard_Click(object sender, RoutedEventArgs e) } } - Process.Start(new ProcessStartInfo() + using (Process.Start(new ProcessStartInfo() { FileName = modelcardUrl, UseShellExecute = true - }); + })) + { + } } } } @@ -431,11 +435,13 @@ private void ViewLicense_Click(object sender, RoutedEventArgs e) licenseUrl = details.Url; } - Process.Start(new ProcessStartInfo() + using (Process.Start(new ProcessStartInfo() { FileName = licenseUrl, UseShellExecute = true - }); + })) + { + } } } @@ -605,11 +611,13 @@ private void ViewModelDetails_Click(object sender, RoutedEventArgs e) return; } - Process.Start(new ProcessStartInfo + using (Process.Start(new ProcessStartInfo { FileName = modelDetailsUrl, UseShellExecute = true - }); + })) + { + } } } } \ No newline at end of file diff --git a/AIDevGallery/Controls/OpacityMask.xaml.cs b/AIDevGallery/Controls/OpacityMask.xaml.cs index feb92c7e..87d26411 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 @@ -33,6 +34,8 @@ public partial class OpacityMaskView : ContentControl private readonly Compositor _compositor = CompositionTarget.GetCompositorForCurrentThread(); private CompositionBrush? _mask; private CompositionMaskBrush? _maskBrush; + private SpriteVisual? _redirectVisual; + private bool _disposed; /// /// Initializes a new instance of the class. @@ -61,15 +64,18 @@ protected override void OnApplyTemplate() ContentPresenter contentPresenter = (ContentPresenter)GetTemplateChild(ContentPresenterTemplateName); Border maskContainer = (Border)GetTemplateChild(MaskContainerTemplateName); + _maskBrush?.Dispose(); _maskBrush = _compositor.CreateMaskBrush(); _maskBrush.Source = GetVisualBrush(contentPresenter); + _mask?.Dispose(); _mask = GetVisualBrush(maskContainer); _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) @@ -78,9 +84,9 @@ private static CompositionBrush GetVisualBrush(UIElement element) Compositor compositor = visual.Compositor; - CompositionVisualSurface visualSurface = compositor.CreateVisualSurface(); + using CompositionVisualSurface visualSurface = compositor.CreateVisualSurface(); visualSurface.SourceVisual = visual; - ExpressionAnimation sourceSizeAnimation = compositor.CreateExpressionAnimation($"{nameof(visual)}.Size"); + using ExpressionAnimation sourceSizeAnimation = compositor.CreateExpressionAnimation($"{nameof(visual)}.Size"); sourceSizeAnimation.SetReferenceParameter(nameof(visual), visual); visualSurface.StartAnimation(nameof(visualSurface.SourceSize), sourceSizeAnimation); @@ -102,4 +108,21 @@ private static void OnOpacityMaskChanged(DependencyObject d, DependencyPropertyC UIElement? opacityMask = (UIElement?)e.NewValue; maskBrush.Mask = opacityMask is null ? null : self._mask; } + + /// + /// Disposes the composition resources used by this control. + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + _mask?.Dispose(); + _maskBrush?.Dispose(); + _redirectVisual?.Dispose(); + _compositor?.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..8745a3a4 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)); @@ -111,6 +111,7 @@ private void CancelCTS() if (_sampleLoadingCts != null) { _sampleLoadingCts.Cancel(); + _sampleLoadingCts.Dispose(); _sampleLoadingCts = null; } } @@ -213,6 +214,7 @@ public async Task LoadSampleAsync(Sample? sample, List? models, Wi return; } + _sampleLoadingCts?.Dispose(); _sampleLoadingCts = new CancellationTokenSource(); var token = _sampleLoadingCts.Token; @@ -311,10 +313,12 @@ public async Task LoadSampleAsync(Sample? sample, List? models, Wi finally { _sampleLoadedCompletionSource = null; + _sampleLoadingCts?.Dispose(); _sampleLoadingCts = null; } _sampleLoadedCompletionSource = null; + _sampleLoadingCts?.Dispose(); _sampleLoadingCts = null; NavigatedToSampleLoadedEvent.Log(sample.Name ?? string.Empty); @@ -587,4 +591,9 @@ private void FooterGrid_SizeChanged(object sender, SizeChangedEventArgs e) } } } + + public void Dispose() + { + _sampleLoadingCts?.Dispose(); + } } \ No newline at end of file diff --git a/AIDevGallery/Controls/Shimmer.xaml.cs b/AIDevGallery/Controls/Shimmer.xaml.cs index 2ec342b8..e589b2c8 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,7 @@ 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; - } + Dispose(); } private void OnActualThemeChanged(FrameworkElement sender, object args) @@ -143,22 +130,34 @@ private bool TryInitializationResource() return false; } - var compositor = _shape.GetVisual().Compositor; - - _rectangleGeometry = compositor.CreateRoundedRectangleGeometry(); - _shapeVisual = compositor.CreateShapeVisual(); - _shimmerMaskGradient = compositor.CreateLinearGradientBrush(); - _gradientStop1 = compositor.CreateColorGradientStop(); - _gradientStop2 = compositor.CreateColorGradientStop(); - _gradientStop3 = compositor.CreateColorGradientStop(); - _gradientStop4 = compositor.CreateColorGradientStop(); - SetGradientAndStops(); - SetGradientStopColorsByTheme(); - _rectangleGeometry.CornerRadius = new Vector2((float)CornerRadius.TopLeft); - var spriteShape = compositor.CreateSpriteShape(_rectangleGeometry); - spriteShape.FillBrush = _shimmerMaskGradient; - _shapeVisual.Shapes.Add(spriteShape); - ElementCompositionPreview.SetElementChildVisual(_shape, _shapeVisual); + using (var shapeVisual = _shape.GetVisual()) + { + var compositor = shapeVisual.Compositor; + + _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); + _spriteShape?.Dispose(); + _spriteShape = compositor.CreateSpriteShape(_rectangleGeometry); + _spriteShape.FillBrush = _shimmerMaskGradient; + _shapeVisual.Shapes.Clear(); + _shapeVisual.Shapes.Add(_spriteShape); + ElementCompositionPreview.SetElementChildVisual(_shape, _shapeVisual); + } _initialized = true; return true; @@ -207,24 +206,33 @@ private void TryStartAnimation() return; } - var rootVisual = _shape.GetVisual(); - _sizeAnimation = rootVisual.GetReference().Size; - _shapeVisual.StartAnimation(nameof(ShapeVisual.Size), _sizeAnimation); - _rectangleGeometry.StartAnimation(nameof(CompositionRoundedRectangleGeometry.Size), _sizeAnimation); - - _gradientStartPointAnimation = rootVisual.Compositor.CreateVector2KeyFrameAnimation(); - _gradientStartPointAnimation.Duration = Duration; - _gradientStartPointAnimation.IterationBehavior = AnimationIterationBehavior.Forever; - _gradientStartPointAnimation.InsertKeyFrame(0.0f, new Vector2(InitialStartPointX, 0.0f)); - _gradientStartPointAnimation.InsertKeyFrame(1.0f, Vector2.Zero); - _shimmerMaskGradient!.StartAnimation(nameof(CompositionLinearGradientBrush.StartPoint), _gradientStartPointAnimation); - - _gradientEndPointAnimation = rootVisual.Compositor.CreateVector2KeyFrameAnimation(); - _gradientEndPointAnimation.Duration = Duration; - _gradientEndPointAnimation.IterationBehavior = AnimationIterationBehavior.Forever; - _gradientEndPointAnimation.InsertKeyFrame(0.0f, new Vector2(1.0f, 0.0f)); - _gradientEndPointAnimation.InsertKeyFrame(1.0f, new Vector2(-InitialStartPointX, 1.0f)); - _shimmerMaskGradient.StartAnimation(nameof(CompositionLinearGradientBrush.EndPoint), _gradientEndPointAnimation); + using (var rootVisual = _shape.GetVisual()) + { + using (var reference = rootVisual.GetReference()) + { + _sizeAnimation?.Dispose(); + _sizeAnimation = reference.Size; + } + + _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; + _gradientStartPointAnimation.InsertKeyFrame(0.0f, new Vector2(InitialStartPointX, 0.0f)); + _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; + _gradientEndPointAnimation.InsertKeyFrame(0.0f, new Vector2(1.0f, 0.0f)); + _gradientEndPointAnimation.InsertKeyFrame(1.0f, new Vector2(-InitialStartPointX, 1.0f)); + _shimmerMaskGradient.StartAnimation(nameof(CompositionLinearGradientBrush.EndPoint), _gradientEndPointAnimation); + } _animationStarted = true; } @@ -260,4 +268,32 @@ 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(); + + _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 a2bf51af..a0128a9a 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 cancelationToken = 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/Pages/APIs/APIPage.xaml.cs b/AIDevGallery/Pages/APIs/APIPage.xaml.cs index acd20914..ef114e35 100644 --- a/AIDevGallery/Pages/APIs/APIPage.xaml.cs +++ b/AIDevGallery/Pages/APIs/APIPage.xaml.cs @@ -184,11 +184,13 @@ private void MarkdownTextBlock_OnLinkClicked(object sender, CommunityToolkit.Lab } ModelDetailsLinkClickedEvent.Log(link); - Process.Start(new ProcessStartInfo() + using (Process.Start(new ProcessStartInfo() { FileName = link, UseShellExecute = true - }); + })) + { + } } 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 18d340cd..d1ec1137 100644 --- a/AIDevGallery/Pages/Models/ModelPage.xaml.cs +++ b/AIDevGallery/Pages/Models/ModelPage.xaml.cs @@ -252,20 +252,24 @@ private void ToolkitActionFlyoutItem_Click(object sender, RoutedEventArgs e) bool wasDeeplinkSuccesful = true; try { - Process.Start(new ProcessStartInfo() + using (Process.Start(new ProcessStartInfo() { FileName = toolkitDeeplink, UseShellExecute = true - }); + })) + { + } } catch { - Process.Start(new ProcessStartInfo() + using (Process.Start(new ProcessStartInfo() { FileName = "https://learn.microsoft.com/en-us/windows/ai/toolkit/", UseShellExecute = true - }); - wasDeeplinkSuccesful = false; + })) + { + wasDeeplinkSuccesful = false; + } } finally { @@ -319,7 +323,9 @@ private void MarkdownTextBlock_OnLinkClicked(object sender, CommunityToolkit.Lab FileName = uri.AbsoluteUri, UseShellExecute = true }; - Process.Start(psi); + using (Process.Start(psi)) + { + } } 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..894e4b7d 100644 --- a/AIDevGallery/Pages/SettingsPage.xaml.cs +++ b/AIDevGallery/Pages/SettingsPage.xaml.cs @@ -95,7 +95,9 @@ private void FolderPathTxt_Click(object sender, RoutedEventArgs e) { if (cacheFolderPath != null) { - Process.Start("explorer.exe", cacheFolderPath); + using (Process.Start("explorer.exe", cacheFolderPath)) + { + } } } @@ -201,7 +203,9 @@ private void ModelFolder_Click(object sender, RoutedEventArgs e) if (path != null && Directory.Exists(path)) { - Process.Start("explorer.exe", path); + using (Process.Start("explorer.exe", 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> embeddingsCollection = vectorStore.GetDynamicCollection("embeddings", StringData.VectorStoreDefinition); + using VectorStore? vectorStore = new InMemoryVectorStore(); + using VectorStoreCollection> embeddingsCollection = vectorStore.GetDynamicCollection("embeddings", StringData.VectorStoreDefinition); await embeddingsCollection.EnsureCollectionExistsAsync(ct).ConfigureAwait(false); List sourceContent = ChunkSourceText(sourceText, 512); diff --git a/AIDevGallery/Samples/Open Source Models/Image Models/ESRGAN/SuperResolution.xaml.cs b/AIDevGallery/Samples/Open Source Models/Image Models/ESRGAN/SuperResolution.xaml.cs index 7855fade..685ff822 100644 --- a/AIDevGallery/Samples/Open Source Models/Image Models/ESRGAN/SuperResolution.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Image Models/ESRGAN/SuperResolution.xaml.cs @@ -38,7 +38,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.ESRGAN; Name = "Enhance Image", Id = "9b74cdc1-f5f7-430f-bed0-712ffc063508", Icon = "\uE8B3")] -internal sealed partial class SuperResolution : BaseSamplePage +internal sealed partial class SuperResolution : BaseSamplePage, IDisposable { private InferenceSession? _inferenceSession; @@ -50,6 +50,11 @@ public SuperResolution() this.InitializeComponent(); } + public void Dispose() + { + _inferenceSession?.Dispose(); + } + private void Page_Loaded() { UploadButton.Focus(FocusState.Programmatic); @@ -92,7 +97,7 @@ private Task InitModel(string modelPath, ExecutionProviderDevicePolicy? policy, Debug.WriteLine($"WARNING: Failed to install packages: {ex.Message}"); } - SessionOptions sessionOptions = new(); + using SessionOptions sessionOptions = new(); sessionOptions.RegisterOrtExtensions(); if (policy != null) @@ -109,6 +114,7 @@ private Task InitModel(string modelPath, ExecutionProviderDevicePolicy? policy, } } + _inferenceSession?.Dispose(); _inferenceSession = new InferenceSession(modelPath, sessionOptions); }); } diff --git a/AIDevGallery/Samples/Open Source Models/Image Models/FFNet/SegmentStreets.xaml.cs b/AIDevGallery/Samples/Open Source Models/Image Models/FFNet/SegmentStreets.xaml.cs index c982b379..e9bb07b4 100644 --- a/AIDevGallery/Samples/Open Source Models/Image Models/FFNet/SegmentStreets.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Image Models/FFNet/SegmentStreets.xaml.cs @@ -39,7 +39,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.FFNet; ], Id = "9b74acc0-a5f7-430f-bed0-958ffc063598", Icon = "\uE8B3")] -internal sealed partial class SegmentStreets : BaseSamplePage +internal sealed partial class SegmentStreets : BaseSamplePage, IDisposable { private InferenceSession? _inferenceSession; public SegmentStreets() @@ -49,6 +49,11 @@ public SegmentStreets() this.InitializeComponent(); } + public void Dispose() + { + _inferenceSession?.Dispose(); + } + // private void Page_Loaded() { @@ -92,7 +97,7 @@ private Task InitModel(string modelPath, ExecutionProviderDevicePolicy? policy, Debug.WriteLine($"WARNING: Failed to install packages: {ex.Message}"); } - SessionOptions sessionOptions = new(); + using SessionOptions sessionOptions = new(); sessionOptions.RegisterOrtExtensions(); if (policy != null) @@ -109,6 +114,7 @@ private Task InitModel(string modelPath, ExecutionProviderDevicePolicy? policy, } } + _inferenceSession?.Dispose(); _inferenceSession = new InferenceSession(modelPath, sessionOptions); }); } diff --git a/AIDevGallery/Samples/Open Source Models/Image Models/FaceDetLite/FaceDetection.xaml.cs b/AIDevGallery/Samples/Open Source Models/Image Models/FaceDetLite/FaceDetection.xaml.cs index 7272c915..90e19114 100644 --- a/AIDevGallery/Samples/Open Source Models/Image Models/FaceDetLite/FaceDetection.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Image Models/FaceDetLite/FaceDetection.xaml.cs @@ -44,7 +44,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.FaceDetLite; Id = "9b74ccc0-f5f7-417f-bed0-712ffc063508", Icon = "\uE8B3")] -internal sealed partial class FaceDetection : BaseSamplePage +internal sealed partial class FaceDetection : BaseSamplePage, IDisposable { private InferenceSession? _inferenceSession; private List predictions = []; @@ -54,6 +54,13 @@ internal sealed partial class FaceDetection : BaseSamplePage private bool modelActive = true; + public void Dispose() + { + _inferenceSession?.Dispose(); + _frameRateTimer?.Stop(); + _frameProcessingLock?.Dispose(); + } + private DateTimeOffset lastFaceDetectionCount = DateTimeOffset.Now; private int faceDetectionsCount; private int faceDetectionsPerSecond; @@ -139,7 +146,7 @@ private Task InitModel(string modelPath, ExecutionProviderDevicePolicy? policy, Debug.WriteLine($"WARNING: Failed to install packages: {ex.Message}"); } - SessionOptions sessionOptions = new(); + using SessionOptions sessionOptions = new(); sessionOptions.RegisterOrtExtensions(); if (policy != null) @@ -156,6 +163,7 @@ private Task InitModel(string modelPath, ExecutionProviderDevicePolicy? policy, } } + _inferenceSession?.Dispose(); _inferenceSession = new InferenceSession(modelPath, sessionOptions); }); } diff --git a/AIDevGallery/Samples/Open Source Models/Image Models/Faster RCNN/ObjectDetection.xaml.cs b/AIDevGallery/Samples/Open Source Models/Image Models/Faster RCNN/ObjectDetection.xaml.cs index 95bbb063..eceaf603 100644 --- a/AIDevGallery/Samples/Open Source Models/Image Models/Faster RCNN/ObjectDetection.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Image Models/Faster RCNN/ObjectDetection.xaml.cs @@ -38,7 +38,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.ObjectDetection.FasterRCNN; Name = "Faster RCNN Object Detection", Id = "9b74ccc0-f5f7-430f-bed0-758ffc063508", Icon = "\uE8B3")] -internal sealed partial class ObjectDetection : BaseSamplePage +internal sealed partial class ObjectDetection : BaseSamplePage, IDisposable { private InferenceSession? _inferenceSession; @@ -49,6 +49,11 @@ public ObjectDetection() this.InitializeComponent(); } + public void Dispose() + { + _inferenceSession?.Dispose(); + } + protected override async Task LoadModelAsync(SampleNavigationParameters sampleParams) { try @@ -93,7 +98,7 @@ private Task InitModel(string modelPath, ExecutionProviderDevicePolicy? policy, Debug.WriteLine($"WARNING: Failed to install packages: {ex.Message}"); } - SessionOptions sessionOptions = new(); + using SessionOptions sessionOptions = new(); sessionOptions.RegisterOrtExtensions(); if (policy != null) @@ -110,6 +115,7 @@ private Task InitModel(string modelPath, ExecutionProviderDevicePolicy? policy, } } + _inferenceSession?.Dispose(); _inferenceSession = new InferenceSession(modelPath, sessionOptions); }); } diff --git a/AIDevGallery/Samples/Open Source Models/Image Models/HRNetPose/PoseDetection.xaml.cs b/AIDevGallery/Samples/Open Source Models/Image Models/HRNetPose/PoseDetection.xaml.cs index 73d7687c..fc6fb795 100644 --- a/AIDevGallery/Samples/Open Source Models/Image Models/HRNetPose/PoseDetection.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Image Models/HRNetPose/PoseDetection.xaml.cs @@ -38,7 +38,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.HRNetPose; Name = "Pose Detection", Id = "9b74ccc0-f5f7-430f-bed0-712ffc063508", Icon = "\uE8B3")] -internal sealed partial class PoseDetection : BaseSamplePage +internal sealed partial class PoseDetection : BaseSamplePage, IDisposable { private InferenceSession? _inferenceSession; public PoseDetection() @@ -48,6 +48,11 @@ public PoseDetection() this.InitializeComponent(); } + public void Dispose() + { + _inferenceSession?.Dispose(); + } + // private void Page_Loaded() { @@ -91,7 +96,7 @@ private Task InitModel(string modelPath, ExecutionProviderDevicePolicy? policy, Debug.WriteLine($"WARNING: Failed to install packages: {ex.Message}"); } - SessionOptions sessionOptions = new(); + using SessionOptions sessionOptions = new(); sessionOptions.RegisterOrtExtensions(); if (policy != null) @@ -108,6 +113,7 @@ private Task InitModel(string modelPath, ExecutionProviderDevicePolicy? policy, } } + _inferenceSession?.Dispose(); _inferenceSession = new InferenceSession(modelPath, sessionOptions); }); } diff --git a/AIDevGallery/Samples/Open Source Models/Image Models/ImageNet/ImageClassification.xaml.cs b/AIDevGallery/Samples/Open Source Models/Image Models/ImageNet/ImageClassification.xaml.cs index f5301125..2e5b46ea 100644 --- a/AIDevGallery/Samples/Open Source Models/Image Models/ImageNet/ImageClassification.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Image Models/ImageNet/ImageClassification.xaml.cs @@ -36,7 +36,7 @@ namespace AIDevGallery.Samples.OpenSourceModels; Name = "ImageNet Image Classification", Id = "09d73ba7-b877-45f9-9de6-41898ab4d339", Icon = "\uE8B9")] -internal sealed partial class ImageClassification : BaseSamplePage +internal sealed partial class ImageClassification : BaseSamplePage, IDisposable { private InferenceSession? _inferenceSession; @@ -47,6 +47,11 @@ public ImageClassification() this.InitializeComponent(); } + public void Dispose() + { + _inferenceSession?.Dispose(); + } + protected override async Task LoadModelAsync(SampleNavigationParameters sampleParams) { try @@ -96,7 +101,7 @@ private Task InitModel(string modelPath, ExecutionProviderDevicePolicy? policy, Debug.WriteLine($"WARNING: Failed to install packages: {ex.Message}"); } - SessionOptions sessionOptions = new(); + using SessionOptions sessionOptions = new(); sessionOptions.RegisterOrtExtensions(); if (policy != null) @@ -113,6 +118,7 @@ private Task InitModel(string modelPath, ExecutionProviderDevicePolicy? policy, } } + _inferenceSession?.Dispose(); _inferenceSession = new InferenceSession(modelPath, sessionOptions); }); } diff --git a/AIDevGallery/Samples/Open Source Models/Image Models/MultiHRNetPose/Multipose.xaml.cs b/AIDevGallery/Samples/Open Source Models/Image Models/MultiHRNetPose/Multipose.xaml.cs index 0f2a0983..0211d4b7 100644 --- a/AIDevGallery/Samples/Open Source Models/Image Models/MultiHRNetPose/Multipose.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Image Models/MultiHRNetPose/Multipose.xaml.cs @@ -115,7 +115,7 @@ private Task GetInferenceSession(string modelPath, ExecutionPr Debug.WriteLine($"WARNING: Failed to install packages: {ex.Message}"); } - SessionOptions sessionOptions = new(); + using SessionOptions sessionOptions = new(); sessionOptions.RegisterOrtExtensions(); if (policy != null) diff --git a/AIDevGallery/Samples/Open Source Models/Image Models/SINet/DetectBackground.xaml.cs b/AIDevGallery/Samples/Open Source Models/Image Models/SINet/DetectBackground.xaml.cs index 75848e6e..11527684 100644 --- a/AIDevGallery/Samples/Open Source Models/Image Models/SINet/DetectBackground.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Image Models/SINet/DetectBackground.xaml.cs @@ -41,7 +41,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.SINet; Id = "9b74ccc0-15f7-430f-red1-7581fd163509", Icon = "\uE8B3")] -internal sealed partial class DetectBackground : BaseSamplePage +internal sealed partial class DetectBackground : BaseSamplePage, IDisposable { private InferenceSession? _inferenceSession; @@ -53,6 +53,11 @@ public DetectBackground() this.InitializeComponent(); } + public void Dispose() + { + _inferenceSession?.Dispose(); + } + private void Page_Loaded() { UploadButton.Focus(FocusState.Programmatic); @@ -95,7 +100,7 @@ private Task InitModel(string modelPath, ExecutionProviderDevicePolicy? policy, Debug.WriteLine($"WARNING: Failed to install packages: {ex.Message}"); } - SessionOptions sessionOptions = new(); + using SessionOptions sessionOptions = new(); sessionOptions.RegisterOrtExtensions(); if (policy != null) @@ -112,6 +117,7 @@ private Task InitModel(string modelPath, ExecutionProviderDevicePolicy? policy, } } + _inferenceSession?.Dispose(); _inferenceSession = new InferenceSession(modelPath, sessionOptions); }); } @@ -154,45 +160,46 @@ private async Task Detect(string filePath) DefaultImage.Source = new BitmapImage(new Uri(filePath)); NarratorHelper.AnnounceImageChanged(DefaultImage, "Image changed: new upload."); // + using ( + Bitmap image = new(filePath)) + { + int originalImageWidth = image.Width; + int originalImageHeight = image.Height; - Bitmap image = new(filePath); - int originalImageWidth = image.Width; - int originalImageHeight = image.Height; - - var inputMetadataName = _inferenceSession.InputNames[0]; - var inputDimensions = _inferenceSession.InputMetadata[inputMetadataName].Dimensions; + var inputMetadataName = _inferenceSession.InputNames[0]; + var inputDimensions = _inferenceSession.InputMetadata[inputMetadataName].Dimensions; - int modelInputHeight = inputDimensions[2]; - int modelInputWidth = inputDimensions[3]; + int modelInputHeight = inputDimensions[2]; + int modelInputWidth = inputDimensions[3]; - var backgroundMask = await Task.Run(() => - { - using var resizedImage = BitmapFunctions.ResizeWithPadding(image, modelInputWidth, modelInputHeight); + var backgroundMask = await Task.Run(() => + { + using var resizedImage = BitmapFunctions.ResizeWithPadding(image, modelInputWidth, modelInputHeight); - Tensor input = new DenseTensor(inputDimensions); - input = BitmapFunctions.PreprocessBitmapWithoutStandardization(resizedImage, input); + Tensor input = new DenseTensor(inputDimensions); + input = BitmapFunctions.PreprocessBitmapWithoutStandardization(resizedImage, input); - var inputs = new List - { + var inputs = new List + { NamedOnnxValue.CreateFromTensor(inputMetadataName, input) - }; + }; - using IDisposableReadOnlyCollection results = _inferenceSession!.Run(inputs); - IEnumerable output = results[0].AsEnumerable(); - return BackgroundHelpers.GetForegroundMask(output, modelInputWidth, modelInputHeight, originalImageWidth, originalImageHeight); - }); + using IDisposableReadOnlyCollection results = _inferenceSession!.Run(inputs); + IEnumerable output = results[0].AsEnumerable(); + return BackgroundHelpers.GetForegroundMask(output, modelInputWidth, modelInputHeight, originalImageWidth, originalImageHeight); + }); - BitmapImage? outputImage = BitmapFunctions.RenderBackgroundMask(image, backgroundMask, originalImageWidth, originalImageHeight); + BitmapImage? outputImage = BitmapFunctions.RenderBackgroundMask(image, backgroundMask, originalImageWidth, originalImageHeight); - DispatcherQueue.TryEnqueue(() => - { - DefaultImage.Source = outputImage!; - Loader.IsActive = false; - Loader.Visibility = Visibility.Collapsed; - UploadButton.Visibility = Visibility.Visible; - }); + DispatcherQueue.TryEnqueue(() => + { + DefaultImage.Source = outputImage!; + Loader.IsActive = false; + Loader.Visibility = Visibility.Collapsed; + UploadButton.Visibility = Visibility.Visible; + }); - NarratorHelper.AnnounceImageChanged(DefaultImage, "Image changed: objects detected."); // - image.Dispose(); + NarratorHelper.AnnounceImageChanged(DefaultImage, "Image changed: objects detected."); // + } } } \ No newline at end of file diff --git a/AIDevGallery/Samples/Open Source Models/Image Models/YOLOv4/YOLOObjectionDetection.xaml.cs b/AIDevGallery/Samples/Open Source Models/Image Models/YOLOv4/YOLOObjectionDetection.xaml.cs index 0b7aeb17..2a17e367 100644 --- a/AIDevGallery/Samples/Open Source Models/Image Models/YOLOv4/YOLOObjectionDetection.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Image Models/YOLOv4/YOLOObjectionDetection.xaml.cs @@ -40,7 +40,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.YOLOv4; Id = "9b74ccc0-15f7-430f-bed0-7581fd163508", Icon = "\uE8B3")] -internal sealed partial class YOLOObjectionDetection : BaseSamplePage +internal sealed partial class YOLOObjectionDetection : BaseSamplePage, IDisposable { private InferenceSession? _inferenceSession; @@ -52,6 +52,11 @@ public YOLOObjectionDetection() this.InitializeComponent(); } + public void Dispose() + { + _inferenceSession?.Dispose(); + } + private void Page_Loaded() { UploadButton.Focus(FocusState.Programmatic); @@ -95,7 +100,7 @@ private Task InitModel(string modelPath, ExecutionProviderDevicePolicy? policy, Debug.WriteLine($"WARNING: Failed to install packages: {ex.Message}"); } - SessionOptions sessionOptions = new(); + using SessionOptions sessionOptions = new(); sessionOptions.RegisterOrtExtensions(); if (policy != null) @@ -112,6 +117,7 @@ private Task InitModel(string modelPath, ExecutionProviderDevicePolicy? policy, } } + _inferenceSession?.Dispose(); _inferenceSession = new InferenceSession(modelPath, sessionOptions); }); } @@ -158,77 +164,77 @@ private async Task DetectObjects(string filePath) DefaultImage.Source = new BitmapImage(new Uri(filePath)); NarratorHelper.AnnounceImageChanged(DefaultImage, "Photo changed: new upload."); // - - Bitmap image = new(filePath); - - int originalWidth = image.Width; - int originalHeight = image.Height; - - var predictions = await Task.Run(() => + using ( + Bitmap image = new(filePath)) { - // Set up - var inputName = _inferenceSession.InputNames[0]; - var inputDimensions = _inferenceSession.InputMetadata[inputName].Dimensions; + int originalWidth = image.Width; + int originalHeight = image.Height; + + var predictions = await Task.Run(() => + { + // Set up + var inputName = _inferenceSession.InputNames[0]; + var inputDimensions = _inferenceSession.InputMetadata[inputName].Dimensions; - // Set batch size - int batchSize = 1; - inputDimensions[0] = batchSize; + // Set batch size + int batchSize = 1; + inputDimensions[0] = batchSize; - // I know the input dimensions to be [batchSize, 416, 416, 3] - int inputWidth = inputDimensions[1]; - int inputHeight = inputDimensions[2]; + // I know the input dimensions to be [batchSize, 416, 416, 3] + int inputWidth = inputDimensions[1]; + int inputHeight = inputDimensions[2]; - using var resizedImage = BitmapFunctions.ResizeWithPadding(image, inputWidth, inputHeight); + using var resizedImage = BitmapFunctions.ResizeWithPadding(image, inputWidth, inputHeight); - // Preprocessing - Tensor input = new DenseTensor(inputDimensions); - input = BitmapFunctions.PreprocessBitmapForYOLO(resizedImage, input); + // Preprocessing + Tensor input = new DenseTensor(inputDimensions); + input = BitmapFunctions.PreprocessBitmapForYOLO(resizedImage, input); - // Setup inputs and outputs - var inputMetadataName = _inferenceSession!.InputNames[0]; - var inputs = new List - { + // Setup inputs and outputs + var inputMetadataName = _inferenceSession!.InputNames[0]; + var inputs = new List + { NamedOnnxValue.CreateFromTensor(inputMetadataName, input) - }; + }; - // Run inference - using IDisposableReadOnlyCollection results = _inferenceSession!.Run(inputs); + // Run inference + using IDisposableReadOnlyCollection results = _inferenceSession!.Run(inputs); - // Extract tensors from inference results - var outputTensor1 = results[0].AsTensor(); - var outputTensor2 = results[1].AsTensor(); - var outputTensor3 = results[2].AsTensor(); + // Extract tensors from inference results + var outputTensor1 = results[0].AsTensor(); + var outputTensor2 = results[1].AsTensor(); + var outputTensor3 = results[2].AsTensor(); - // Define anchors (as per your model) - var anchors = new List<(float Width, float Height)> - { + // Define anchors (as per your model) + var anchors = new List<(float Width, float Height)> + { (12, 16), (19, 36), (40, 28), // Small grid (52x52) (36, 75), (76, 55), (72, 146), // Medium grid (26x26) (142, 110), (192, 243), (459, 401) // Large grid (13x13) - }; + }; - // Combine tensors into a list for processing - var gridTensors = new List> { outputTensor1, outputTensor2, outputTensor3 }; + // Combine tensors into a list for processing + var gridTensors = new List> { outputTensor1, outputTensor2, outputTensor3 }; - // Postprocessing steps - var extractedPredictions = YOLOHelpers.ExtractPredictions(gridTensors, anchors, inputWidth, inputHeight, originalWidth, originalHeight); - var filteredPredictions = YOLOHelpers.ApplyNms(extractedPredictions, .4f); + // Postprocessing steps + var extractedPredictions = YOLOHelpers.ExtractPredictions(gridTensors, anchors, inputWidth, inputHeight, originalWidth, originalHeight); + var filteredPredictions = YOLOHelpers.ApplyNms(extractedPredictions, .4f); - // Return the final predictions - return filteredPredictions; - }); + // Return the final predictions + return filteredPredictions; + }); - BitmapImage outputImage = BitmapFunctions.RenderPredictions(image, predictions); + BitmapImage outputImage = BitmapFunctions.RenderPredictions(image, predictions); - DispatcherQueue.TryEnqueue(() => - { - DefaultImage.Source = outputImage; - Loader.IsActive = false; - Loader.Visibility = Visibility.Collapsed; - UploadButton.Visibility = Visibility.Visible; - }); + DispatcherQueue.TryEnqueue(() => + { + DefaultImage.Source = outputImage; + Loader.IsActive = false; + Loader.Visibility = Visibility.Collapsed; + UploadButton.Visibility = Visibility.Visible; + }); - NarratorHelper.AnnounceImageChanged(DefaultImage, "Photo changed: objects detected."); // - image.Dispose(); + NarratorHelper.AnnounceImageChanged(DefaultImage, "Photo changed: objects detected."); // + } } } \ No newline at end of file diff --git a/AIDevGallery/Samples/Open Source Models/Language Models/Chat.xaml.cs b/AIDevGallery/Samples/Open Source Models/Language Models/Chat.xaml.cs index c14587d8..c7ae6829 100644 --- a/AIDevGallery/Samples/Open Source Models/Language Models/Chat.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Language Models/Chat.xaml.cs @@ -33,7 +33,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.LanguageModels; SharedCodeEnum.Message, SharedCodeEnum.ChatTemplateSelector, ])] -internal sealed partial class Chat : BaseSamplePage +internal sealed partial class Chat : BaseSamplePage, IDisposable { private CancellationTokenSource? cts; public ObservableCollection Messages { get; } = []; @@ -60,6 +60,7 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa { try { + model?.Dispose(); model = await sampleParams.GetIChatClientAsync(); } catch (Exception ex) @@ -85,6 +86,11 @@ private void CleanUp() model?.Dispose(); } + public void Dispose() + { + CleanUp(); + } + private void CancelResponse() { StopBtn.Visibility = Visibility.Collapsed; @@ -169,6 +175,7 @@ private void AddMessage(string text) InputBox.PlaceholderText = "Please wait for the response to complete before entering a new prompt"; }); + cts?.Dispose(); cts = new CancellationTokenSource(); history.Insert(0, new ChatMessage(ChatRole.System, "You are a helpful assistant")); diff --git a/AIDevGallery/Samples/Open Source Models/Language Models/ContentModeration.xaml.cs b/AIDevGallery/Samples/Open Source Models/Language Models/ContentModeration.xaml.cs index bbb7b30b..29929179 100644 --- a/AIDevGallery/Samples/Open Source Models/Language Models/ContentModeration.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Language Models/ContentModeration.xaml.cs @@ -24,7 +24,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.LanguageModels; SharedCode = [], Id = "language-content-moderation", Icon = "\uE8D4")] -internal sealed partial class ContentModeration : BaseSamplePage +internal sealed partial class ContentModeration : BaseSamplePage, IDisposable { private IChatClient? model; private CancellationTokenSource? cts; @@ -42,6 +42,7 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa { try { + model?.Dispose(); model = await sampleParams.GetIChatClientAsync(); } catch (Exception ex) @@ -65,6 +66,11 @@ private void CleanUp() model?.Dispose(); } + public void Dispose() + { + CleanUp(); + } + public void GenerateText(string prompt) { if (model == null) @@ -87,6 +93,7 @@ public void GenerateText(string prompt) { string systemPrompt = "You are a helpful assistant."; + cts?.Dispose(); cts = new CancellationTokenSource(); var isProgressVisible = true; diff --git a/AIDevGallery/Samples/Open Source Models/Language Models/CustomSystemPrompt.xaml.cs b/AIDevGallery/Samples/Open Source Models/Language Models/CustomSystemPrompt.xaml.cs index 96ac2829..bf1d46e0 100644 --- a/AIDevGallery/Samples/Open Source Models/Language Models/CustomSystemPrompt.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Language Models/CustomSystemPrompt.xaml.cs @@ -27,7 +27,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.LanguageModels; NugetPackageReferences = [ "Microsoft.Extensions.AI" ])] -internal sealed partial class CustomSystemPrompt : BaseSamplePage, INotifyPropertyChanged +internal sealed partial class CustomSystemPrompt : BaseSamplePage, INotifyPropertyChanged, IDisposable { private readonly int defaultTopK = 50; private readonly float defaultTopP = 0.9f; @@ -71,6 +71,7 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa { try { + chatClient?.Dispose(); chatClient = await sampleParams.GetIChatClientAsync(); chatOptions = GetDefaultChatOptions(chatClient); IsPhiSilica = chatClient?.GetService()?.ProviderName == "PhiSilica"; @@ -136,6 +137,11 @@ private void CleanUp() chatClient?.Dispose(); } + public void Dispose() + { + CleanUp(); + } + public string GetAutomationName(string name, double value) => $"{name} {value:F0}"; public ChatOptions GetDefaultChatOptions(IChatClient? chatClient) @@ -188,6 +194,7 @@ public void GenerateText(string query, string systemPrompt) Task.Run( async () => { + cts?.Dispose(); cts = new CancellationTokenSource(); IsProgressVisible = true; diff --git a/AIDevGallery/Samples/Open Source Models/Language Models/ExplainCode.xaml.cs b/AIDevGallery/Samples/Open Source Models/Language Models/ExplainCode.xaml.cs index e88415ae..fb7db63c 100644 --- a/AIDevGallery/Samples/Open Source Models/Language Models/ExplainCode.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Language Models/ExplainCode.xaml.cs @@ -23,7 +23,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.LanguageModels; Name = "Explain Code", Id = "ad763407-6a97-4916-ab05-30fd22f54252", Icon = "\uE8D4")] -internal sealed partial class ExplainCode : BaseSamplePage +internal sealed partial class ExplainCode : BaseSamplePage, IDisposable { private IChatClient? model; private CancellationTokenSource? cts; @@ -41,6 +41,7 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa { try { + model?.Dispose(); model = await sampleParams.GetIChatClientAsync(); // Increase the default max length to allow larger pieces of code @@ -69,6 +70,11 @@ private void CleanUp() model?.Dispose(); } + public void Dispose() + { + CleanUp(); + } + public bool IsProgressVisible { get => isProgressVisible; @@ -102,6 +108,7 @@ public void Explain(string code) string systemPrompt = "You explain user provided code. Provide an explanation of code and no extraneous text. If you can't find code in the user prompt, reply with \"No Code Found.\""; string userPrompt = "Explain this code: " + code; + cts?.Dispose(); cts = new CancellationTokenSource(); var isProgressVisible = true; diff --git a/AIDevGallery/Samples/Open Source Models/Language Models/Generate.xaml.cs b/AIDevGallery/Samples/Open Source Models/Language Models/Generate.xaml.cs index b8629b94..a9333943 100644 --- a/AIDevGallery/Samples/Open Source Models/Language Models/Generate.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Language Models/Generate.xaml.cs @@ -24,7 +24,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.LanguageModels; ], Id = "25bb4e58-d909-4377-b59c-975cd6baff19", Icon = "\uE8D4")] -internal sealed partial class Generate : BaseSamplePage +internal sealed partial class Generate : BaseSamplePage, IDisposable { private const int _maxTokenLength = 1024; private IChatClient? chatClient; @@ -43,6 +43,7 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa { try { + chatClient?.Dispose(); chatClient = await sampleParams.GetIChatClientAsync(); InputTextBox.MaxLength = _maxTokenLength; } @@ -67,6 +68,11 @@ private void CleanUp() chatClient?.Dispose(); } + public void Dispose() + { + CleanUp(); + } + public bool IsProgressVisible { get => isProgressVisible; @@ -103,6 +109,7 @@ public void GenerateText(string topic) string systemPrompt = "You generate text based on a user-provided topic. Respond with only the generated content and no extraneous text."; string userPrompt = "Generate text based on the topic: " + topic; + cts?.Dispose(); cts = new CancellationTokenSource(); IsProgressVisible = true; diff --git a/AIDevGallery/Samples/Open Source Models/Language Models/GenerateCode.xaml.cs b/AIDevGallery/Samples/Open Source Models/Language Models/GenerateCode.xaml.cs index ce6f8436..e78ba11c 100644 --- a/AIDevGallery/Samples/Open Source Models/Language Models/GenerateCode.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Language Models/GenerateCode.xaml.cs @@ -29,7 +29,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.LanguageModels; Name = "Generate Code", Id = "2270c051-a91c-4af9-8975-a99fda6b024b", Icon = "\uE8D4")] -internal sealed partial class GenerateCode : BaseSamplePage +internal sealed partial class GenerateCode : BaseSamplePage, IDisposable { private const int _defaultMaxLength = 1024; private RichTextBlockFormatter formatter; @@ -54,6 +54,7 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa { try { + chatClient?.Dispose(); chatClient = await sampleParams.GetIChatClientAsync(); InputTextBox.MaxLength = _defaultMaxLength; } @@ -78,6 +79,11 @@ private void CleanUp() chatClient?.Dispose(); } + public void Dispose() + { + CleanUp(); + } + public bool IsProgressVisible { get => isProgressVisible; @@ -110,6 +116,7 @@ public void GenerateSolution(string problem, string currentLanguage) async () => { string systemPrompt = "You generate code in " + currentLanguage + ". Respond with only the code in " + currentLanguage + " and no extraneous text."; + cts?.Dispose(); cts = new CancellationTokenSource(); IsProgressVisible = true; diff --git a/AIDevGallery/Samples/Open Source Models/Language Models/GrammarCheck.xaml.cs b/AIDevGallery/Samples/Open Source Models/Language Models/GrammarCheck.xaml.cs index 66060cfb..d81a34aa 100644 --- a/AIDevGallery/Samples/Open Source Models/Language Models/GrammarCheck.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Language Models/GrammarCheck.xaml.cs @@ -22,7 +22,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.LanguageModels; Name = "Grammar Check", Id = "9e1b5ac5-3521-4e88-a2ce-60152a6cb44f", Icon = "\uE8D4")] -internal sealed partial class GrammarCheck : BaseSamplePage +internal sealed partial class GrammarCheck : BaseSamplePage, IDisposable { private const int _maxTokenLength = 1024; private IChatClient? chatClient; @@ -40,6 +40,7 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa { try { + chatClient?.Dispose(); chatClient = await sampleParams.GetIChatClientAsync(); InputTextBox.MaxLength = _maxTokenLength; } @@ -64,6 +65,11 @@ private void CleanUp() chatClient?.Dispose(); } + public void Dispose() + { + CleanUp(); + } + public bool IsProgressVisible { get => isProgressVisible; @@ -99,6 +105,7 @@ public void GrammarCheckText(string text) string userPrompt = "Grammar check this text: " + text; + cts?.Dispose(); cts = new CancellationTokenSource(); IsProgressVisible = true; diff --git a/AIDevGallery/Samples/Open Source Models/Language Models/Paraphrase.xaml.cs b/AIDevGallery/Samples/Open Source Models/Language Models/Paraphrase.xaml.cs index 998c915e..8f972a01 100644 --- a/AIDevGallery/Samples/Open Source Models/Language Models/Paraphrase.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Language Models/Paraphrase.xaml.cs @@ -22,7 +22,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.LanguageModels; ], Id = "9e006e82-8e3f-4401-8a83-d4c4c59cc20c", Icon = "\uE8D4")] -internal sealed partial class Paraphrase : BaseSamplePage +internal sealed partial class Paraphrase : BaseSamplePage, IDisposable { private const int _defaultMaxLength = 1024; private IChatClient? chatClient; @@ -40,6 +40,7 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa { try { + chatClient?.Dispose(); chatClient = await sampleParams.GetIChatClientAsync(); InputTextBox.MaxLength = _defaultMaxLength; } @@ -64,6 +65,11 @@ private void CleanUp() chatClient?.Dispose(); } + public void Dispose() + { + CleanUp(); + } + public bool IsProgressVisible { get => isProgressVisible; @@ -98,6 +104,7 @@ public void ParaphraseText(string text) "Respond with only the paraphrased content and no extraneous text."; string userPrompt = "Paraphrase this text: " + text; + cts?.Dispose(); cts = new CancellationTokenSource(); IsProgressVisible = true; diff --git a/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml.cs b/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml.cs index 8b982101..45335fbe 100644 --- a/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml.cs @@ -75,9 +75,12 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa return; } + using (model) + { #pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - _chatCompletionService = model.AsChatCompletionService(); + _chatCompletionService = model.AsChatCompletionService(); #pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + } IKernelBuilder builder = Kernel.CreateBuilder(); _semanticKernel = builder.Build(); @@ -179,6 +182,7 @@ private void AddMessage(string text) InputBox.PlaceholderText = "Please wait for the response to complete before entering a new prompt"; }); + cts?.Dispose(); cts = new CancellationTokenSource(); string fullResponse = string.Empty; diff --git a/AIDevGallery/Samples/Open Source Models/Language Models/SentimentAnalysis.xaml.cs b/AIDevGallery/Samples/Open Source Models/Language Models/SentimentAnalysis.xaml.cs index 2d13490b..004bc56d 100644 --- a/AIDevGallery/Samples/Open Source Models/Language Models/SentimentAnalysis.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Language Models/SentimentAnalysis.xaml.cs @@ -23,7 +23,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.LanguageModels; Name = "Sentiment Analysis", Id = "9cc84d1e-6b02-4bd2-a350-6e38c3a92ced", Icon = "\uE8D4")] -internal sealed partial class SentimentAnalysis : BaseSamplePage +internal sealed partial class SentimentAnalysis : BaseSamplePage, IDisposable { private const int _maxTokenLength = 1024; private IChatClient? chatClient; @@ -41,6 +41,7 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa { try { + chatClient?.Dispose(); chatClient = await sampleParams.GetIChatClientAsync(); InputTextBox.MaxLength = _maxTokenLength; } @@ -65,6 +66,11 @@ private void CleanUp() chatClient?.Dispose(); } + public void Dispose() + { + CleanUp(); + } + public bool IsProgressVisible { get => isProgressVisible; @@ -102,6 +108,7 @@ public void AnalyzeSentiment(string text) var userPrompt = "Analyze the sentiment of the following text: " + text; + cts?.Dispose(); cts = new CancellationTokenSource(); var response = string.Empty; diff --git a/AIDevGallery/Samples/Open Source Models/Language Models/SmartPaste.xaml.cs b/AIDevGallery/Samples/Open Source Models/Language Models/SmartPaste.xaml.cs index 29d25a9b..a6632a52 100644 --- a/AIDevGallery/Samples/Open Source Models/Language Models/SmartPaste.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Language Models/SmartPaste.xaml.cs @@ -24,7 +24,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.LanguageModels; SharedCodeEnum.SmartPasteFormCs, SharedCodeEnum.SmartPasteFormXaml ])] -internal sealed partial class SmartPaste : BaseSamplePage +internal sealed partial class SmartPaste : BaseSamplePage, IDisposable { private IChatClient? model; public List FieldLabels { get; set; } = ["Name", "Address", "City", "State", "Zip"]; @@ -47,6 +47,7 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa { try { + model?.Dispose(); model = await sampleParams.GetIChatClientAsync(); } catch (Exception ex) @@ -66,4 +67,9 @@ private void CleanUp() { model?.Dispose(); } + + public void Dispose() + { + CleanUp(); + } } \ No newline at end of file diff --git a/AIDevGallery/Samples/Open Source Models/Language Models/SmartText.xaml.cs b/AIDevGallery/Samples/Open Source Models/Language Models/SmartText.xaml.cs index 293c8679..f2459942 100644 --- a/AIDevGallery/Samples/Open Source Models/Language Models/SmartText.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Language Models/SmartText.xaml.cs @@ -22,7 +22,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.LanguageModels; SharedCodeEnum.SmartTextBoxCs, SharedCodeEnum.SmartTextBoxXaml ])] -internal sealed partial class SmartText : BaseSamplePage +internal sealed partial class SmartText : BaseSamplePage, IDisposable { private IChatClient? _model; @@ -43,6 +43,7 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa { try { + _model?.Dispose(); _model = await sampleParams.GetIChatClientAsync(); } catch (Exception ex) @@ -62,4 +63,9 @@ private void CleanUp() { _model?.Dispose(); } + + public void Dispose() + { + CleanUp(); + } } \ No newline at end of file diff --git a/AIDevGallery/Samples/Open Source Models/Language Models/Summarize.xaml.cs b/AIDevGallery/Samples/Open Source Models/Language Models/Summarize.xaml.cs index a43b1829..05bde7d2 100644 --- a/AIDevGallery/Samples/Open Source Models/Language Models/Summarize.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Language Models/Summarize.xaml.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.AI; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using System; using System.Threading; using System.Threading.Tasks; @@ -21,7 +22,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.LanguageModels; ], Id = "21bf3574-aaa5-42fd-9f6c-3bfbbca00876", Icon = "\uE8D4")] -internal sealed partial class Summarize : BaseSamplePage +internal sealed partial class Summarize : BaseSamplePage, IDisposable { private const int _defaultMaxLength = 1024; private IChatClient? chatClient; @@ -39,6 +40,7 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa { try { + (chatClient as IDisposable)?.Dispose(); chatClient = await sampleParams.GetIChatClientAsync(); InputTextBox.MaxLength = _defaultMaxLength; } @@ -61,6 +63,11 @@ private void CleanUp() chatClient?.Dispose(); } + public void Dispose() + { + CleanUp(); + } + public bool IsProgressVisible { get => isProgressVisible; @@ -95,6 +102,7 @@ public void SummarizeText(string text) "Respond with only the summary itself and no extraneous text."; string userPrompt = "Summarize this text: " + text; + cts?.Dispose(); cts = new CancellationTokenSource(); IsProgressVisible = true; diff --git a/AIDevGallery/Samples/Open Source Models/Language Models/Translate.xaml.cs b/AIDevGallery/Samples/Open Source Models/Language Models/Translate.xaml.cs index 52438fac..b1af5860 100644 --- a/AIDevGallery/Samples/Open Source Models/Language Models/Translate.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Language Models/Translate.xaml.cs @@ -23,7 +23,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.LanguageModels; ], Id = "f045fca2-c657-4894-99f2-d0a1115176bc", Icon = "\uE8D4")] -internal sealed partial class Translate : BaseSamplePage +internal sealed partial class Translate : BaseSamplePage, IDisposable { private const int _defaultMaxLength = 1024; private IChatClient? chatClient; @@ -40,6 +40,7 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa { try { + chatClient?.Dispose(); chatClient = await sampleParams.GetIChatClientAsync(); InputTextBox.MaxLength = _defaultMaxLength; } @@ -64,6 +65,11 @@ private void CleanUp() chatClient?.Dispose(); } + public void Dispose() + { + CleanUp(); + } + public bool IsProgressVisible { get => isProgressVisible; @@ -101,6 +107,7 @@ public void TranslateText(string text) string systemPrompt = "You translate user provided text. Do not reply with any extraneous content besides the translated text itself."; string userPrompt = $@"Translate the following text to {targetLanguage}: '{text}'"; + cts?.Dispose(); cts = new CancellationTokenSource(); IsProgressVisible = true; diff --git a/AIDevGallery/Samples/Open Source Models/Multimodal Models/DescribeImage.xaml.cs b/AIDevGallery/Samples/Open Source Models/Multimodal Models/DescribeImage.xaml.cs index e52e0e2c..586a65d2 100644 --- a/AIDevGallery/Samples/Open Source Models/Multimodal Models/DescribeImage.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Multimodal Models/DescribeImage.xaml.cs @@ -32,7 +32,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.MultimodalModels; "Microsoft.ML.OnnxRuntimeGenAI.WinML" ], Name = "Describe Image")] -internal sealed partial class DescribeImage : BaseSamplePage +internal sealed partial class DescribeImage : BaseSamplePage, IDisposable { private Model? model; private MultiModalProcessor? processor; @@ -70,12 +70,15 @@ await Task.Run( { try { + model?.Dispose(); model = new Model(modelPath); ct.ThrowIfCancellationRequested(); + processor?.Dispose(); processor = new MultiModalProcessor(model); ct.ThrowIfCancellationRequested(); + tokenizerStream?.Dispose(); tokenizerStream = processor.CreateStream(); ct.ThrowIfCancellationRequested(); } @@ -88,7 +91,7 @@ await Task.Run( ct); } - private void Dispose() + public void Dispose() { _cts?.Cancel(); _cts?.Dispose(); @@ -129,7 +132,7 @@ private async IAsyncEnumerable InferStreaming(string question, string im var prompt = $@"<|user|>\n<|image_1|>\n{question}<|end|>\n<|assistant|>\n"; string[] stopTokens = ["", "<|user|>", "<|end|>", "<|assistant|>"]; - var inputTensors = processor.ProcessImages(prompt, images); + using var inputTensors = processor.ProcessImages(prompt, images); using GeneratorParams generatorParams = new(model); generatorParams.SetSearchOption("max_length", 4096); diff --git a/AIDevGallery/Samples/Open Source Models/Stable Diffusion/GenerateImage.xaml.cs b/AIDevGallery/Samples/Open Source Models/Stable Diffusion/GenerateImage.xaml.cs index 0f1fb991..5415a2e7 100644 --- a/AIDevGallery/Samples/Open Source Models/Stable Diffusion/GenerateImage.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Stable Diffusion/GenerateImage.xaml.cs @@ -54,7 +54,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.StableDiffusionImageGeneration; ], Icon = "\uEE71")] -internal sealed partial class GenerateImage : BaseSamplePage +internal sealed partial class GenerateImage : BaseSamplePage, IDisposable { private string prompt = string.Empty; private bool modelReady; @@ -83,6 +83,7 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa try { + stableDiffusion?.Dispose(); stableDiffusion = new StableDiffusion(parentFolder); await stableDiffusion.InitializeAsync(policy, epName, compileModel, deviceType); } @@ -110,6 +111,11 @@ private void CleanUp() stableDiffusion?.Dispose(); } + public void Dispose() + { + CleanUp(); + } + private async void GenerateButton_Click(object sender, RoutedEventArgs e) { await DoStableDiffusion(); diff --git a/AIDevGallery/Samples/Open Source Models/Whisper/WhisperAudioTranscription.xaml.cs b/AIDevGallery/Samples/Open Source Models/Whisper/WhisperAudioTranscription.xaml.cs index 1c00a6f6..ab82b133 100644 --- a/AIDevGallery/Samples/Open Source Models/Whisper/WhisperAudioTranscription.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Whisper/WhisperAudioTranscription.xaml.cs @@ -28,7 +28,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.Whisper; ], Id = "c7e248af-86e8-49ba-9f44-022230963261", Icon = "\uE8D4")] -internal sealed partial class WhisperAudioTranscription : BaseSamplePage +internal sealed partial class WhisperAudioTranscription : BaseSamplePage, IDisposable { private WaveInEvent? waveIn; private MemoryStream? audioStream; @@ -52,6 +52,7 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa { try { + whisper?.Dispose(); whisper = await WhisperWrapper.CreateAsync(sampleParams.ModelPath, sampleParams.WinMlSampleOptions.Policy, sampleParams.WinMlSampleOptions.EpName, sampleParams.WinMlSampleOptions.CompileModel, sampleParams.WinMlSampleOptions.DeviceType); sampleParams.NotifyCompletion(); } @@ -194,4 +195,9 @@ private void DisposeMemory() recordingTimer?.Stop(); recordingTimer?.Dispose(); } + + public void Dispose() + { + DisposeMemory(); + } } \ No newline at end of file diff --git a/AIDevGallery/Samples/Open Source Models/Whisper/WhisperAudioTranslation.xaml.cs b/AIDevGallery/Samples/Open Source Models/Whisper/WhisperAudioTranslation.xaml.cs index 731c6daf..f86a88b9 100644 --- a/AIDevGallery/Samples/Open Source Models/Whisper/WhisperAudioTranslation.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Whisper/WhisperAudioTranslation.xaml.cs @@ -28,7 +28,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.Whisper; ], Id = "a969cb7a-67c3-4675-9ab1-7c5f9f0f8dd6", Icon = "\uE8D4")] -internal sealed partial class WhisperAudioTranslation : BaseSamplePage +internal sealed partial class WhisperAudioTranslation : BaseSamplePage, IDisposable { private WaveInEvent? waveIn; private MemoryStream? audioStream; @@ -47,10 +47,16 @@ public WhisperAudioTranslation() DataContext = this; } + public void Dispose() + { + DisposeMemory(); + } + protected override async Task LoadModelAsync(SampleNavigationParameters sampleParams) { try { + whisper?.Dispose(); whisper = await WhisperWrapper.CreateAsync(sampleParams.ModelPath, sampleParams.WinMlSampleOptions.Policy, sampleParams.WinMlSampleOptions.EpName, sampleParams.WinMlSampleOptions.CompileModel, sampleParams.WinMlSampleOptions.DeviceType); sampleParams.NotifyCompletion(); } diff --git a/AIDevGallery/Samples/Open Source Models/Whisper/WhisperLiveTranscription.xaml.cs b/AIDevGallery/Samples/Open Source Models/Whisper/WhisperLiveTranscription.xaml.cs index ba35d487..40842aa3 100644 --- a/AIDevGallery/Samples/Open Source Models/Whisper/WhisperLiveTranscription.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Whisper/WhisperLiveTranscription.xaml.cs @@ -26,7 +26,7 @@ namespace AIDevGallery.Samples.OpenSourceModels.Whisper; ], Id = "2b13b8ef-a75c-4982-9f7e-eae1a11c87a2", Icon = "\uE8D4")] -internal sealed partial class WhisperLiveTranscription : BaseSamplePage +internal sealed partial class WhisperLiveTranscription : BaseSamplePage, IDisposable { private readonly AudioRecorder audioRecorder; private bool isRecording; @@ -42,10 +42,16 @@ public WhisperLiveTranscription() audioRecorder = new AudioRecorder(UpdateTranscription); } + public void Dispose() + { + DisposeMemory(); + } + protected override async Task LoadModelAsync(SampleNavigationParameters sampleParams) { try { + whisper?.Dispose(); whisper = await WhisperWrapper.CreateAsync(sampleParams.ModelPath, sampleParams.WinMlSampleOptions.Policy, sampleParams.WinMlSampleOptions.EpName, sampleParams.WinMlSampleOptions.CompileModel, sampleParams.WinMlSampleOptions.DeviceType); sampleParams.NotifyCompletion(); } diff --git a/AIDevGallery/Samples/SharedCode/AudioRecorder.cs b/AIDevGallery/Samples/SharedCode/AudioRecorder.cs index e351d382..8e3d43de 100644 --- a/AIDevGallery/Samples/SharedCode/AudioRecorder.cs +++ b/AIDevGallery/Samples/SharedCode/AudioRecorder.cs @@ -9,7 +9,7 @@ namespace AIDevGallery.Samples.SharedCode; -internal class AudioRecorder : IDisposable +internal sealed class AudioRecorder : IDisposable { private const int BufferSize = 32000; // Adjust buffer size if needed private readonly WaveInEvent waveIn; diff --git a/AIDevGallery/Samples/SharedCode/BitmapFunctions.cs b/AIDevGallery/Samples/SharedCode/BitmapFunctions.cs index 00a0df8a..7202620f 100644 --- a/AIDevGallery/Samples/SharedCode/BitmapFunctions.cs +++ b/AIDevGallery/Samples/SharedCode/BitmapFunctions.cs @@ -96,16 +96,20 @@ public static async Task ResizeVideoFrameWithPadding(VideoFrame videoFra stream.Seek(0); // Reset stream position // Convert to System.Drawing.Bitmap - using var tempBitmap = new Bitmap(stream.AsStream()); - Bitmap paddedBitmap = new(targetWidth, targetHeight); - - using (Graphics graphics = Graphics.FromImage(paddedBitmap)) + Bitmap paddedBitmap; + using (var streamWrapper = stream.AsStream()) + using (var tempBitmap = new Bitmap(streamWrapper)) { - graphics.Clear(Color.White); // White padding background - graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; + paddedBitmap = new(targetWidth, targetHeight); + + using (Graphics graphics = Graphics.FromImage(paddedBitmap)) + { + graphics.Clear(Color.White); // White padding background + graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; - // Draw the resized image centered - graphics.DrawImage(tempBitmap, offsetX, offsetY, scaledWidth, scaledHeight); + // Draw the resized image centered + graphics.DrawImage(tempBitmap, offsetX, offsetY, scaledWidth, scaledHeight); + } } return paddedBitmap; @@ -317,7 +321,8 @@ public static BitmapImage RenderPredictions(Bitmap image, List predi memoryStream.Position = 0; - bitmapImage.SetSource(memoryStream.AsRandomAccessStream()); + using var randomAccessStream = memoryStream.AsRandomAccessStream(); + bitmapImage.SetSource(randomAccessStream); } return bitmapImage; @@ -351,7 +356,8 @@ public static BitmapImage RenderPredictions(Bitmap image, List predi { image.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png); memoryStream.Position = 0; - bitmapImage.SetSource(memoryStream.AsRandomAccessStream()); + using var randomAccessStream = memoryStream.AsRandomAccessStream(); + bitmapImage.SetSource(randomAccessStream); } return bitmapImage; @@ -469,7 +475,11 @@ public static BitmapImage ConvertBitmapToBitmapImage(Bitmap bitmap) using var stream = new InMemoryRandomAccessStream(); // Save the bitmap to a stream - bitmap.Save(stream.AsStream(), ImageFormat.Png); + using (var streamWrapper = stream.AsStream()) + { + bitmap.Save(streamWrapper, ImageFormat.Png); + } + stream.Seek(0); // Create a BitmapImage from the stream diff --git a/AIDevGallery/Samples/SharedCode/Controls/SmartPasteForm.cs b/AIDevGallery/Samples/SharedCode/Controls/SmartPasteForm.cs index 250f7429..c321df16 100644 --- a/AIDevGallery/Samples/SharedCode/Controls/SmartPasteForm.cs +++ b/AIDevGallery/Samples/SharedCode/Controls/SmartPasteForm.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using AIDevGallery.Models; using AIDevGallery.Telemetry.Events; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.Extensions.AI; @@ -102,7 +103,7 @@ private async Task> InferPasteValues(string clipboard return []; } - SampleInteractionEvent.SendSampleInteractedEvent(model, Models.ScenarioType.SmartControlsSmartPaste, "InferPasteValues"); // + SampleInteractionEvent.SendSampleInteractedEvent(model, ScenarioType.SmartControlsSmartPaste, "InferPasteValues"); // string outputMessage = string.Empty; PromptInput input = new() { @@ -111,31 +112,30 @@ private async Task> InferPasteValues(string clipboard clipboardText[.._defaultMaxLength] : clipboardText }; - - CancellationTokenSource cts = new(); string output = string.Empty; - - await foreach (var messagePart in model.GetStreamingResponseAsync( - [ - new ChatMessage(ChatRole.System, _systemPrompt), - new ChatMessage(ChatRole.User, JsonSerializer.Serialize(input, SmartPasteSourceGenerationContext.Default.PromptInput)) - ], - null, - cts.Token)) + using ( + CancellationTokenSource cts = new()) { - outputMessage += messagePart; - - Match match = Regex.Match(outputMessage, "{([^}]*)}", RegexOptions.Multiline); - if (match.Success) + await foreach (var messagePart in model.GetStreamingResponseAsync( + [ + new ChatMessage(ChatRole.System, _systemPrompt), + new ChatMessage(ChatRole.User, JsonSerializer.Serialize(input, SmartPasteSourceGenerationContext.Default.PromptInput)) + ], + null, + cts.Token)) { - output = match.Value; - cts.Cancel(); - break; + outputMessage += messagePart; + + Match match = Regex.Match(outputMessage, "{([^}]*)}", RegexOptions.Multiline); + if (match.Success) + { + output = match.Value; + cts.Cancel(); + break; + } } } - cts.Dispose(); - if (string.IsNullOrWhiteSpace(output)) { return []; diff --git a/AIDevGallery/Samples/SharedCode/Embeddings/EmbeddingGenerator.cs b/AIDevGallery/Samples/SharedCode/Embeddings/EmbeddingGenerator.cs index ad608595..98ec65c5 100644 --- a/AIDevGallery/Samples/SharedCode/Embeddings/EmbeddingGenerator.cs +++ b/AIDevGallery/Samples/SharedCode/Embeddings/EmbeddingGenerator.cs @@ -22,7 +22,7 @@ namespace AIDevGallery.Samples.SharedCode; -internal partial class EmbeddingGenerator : IDisposable, IEmbeddingGenerator> +internal sealed partial class EmbeddingGenerator : IDisposable, IEmbeddingGenerator> { [GeneratedRegex(@"[\u0000-\u001F\u007F-\uFFFF]")] private static partial Regex MyRegex(); diff --git a/AIDevGallery/Samples/SharedCode/IChatClient/OnnxRuntimeGenAIChatClientFactory.cs b/AIDevGallery/Samples/SharedCode/IChatClient/OnnxRuntimeGenAIChatClientFactory.cs index bf9a16b0..c04b9891 100644 --- a/AIDevGallery/Samples/SharedCode/IChatClient/OnnxRuntimeGenAIChatClientFactory.cs +++ b/AIDevGallery/Samples/SharedCode/IChatClient/OnnxRuntimeGenAIChatClientFactory.cs @@ -52,12 +52,13 @@ await Task.Run( () => { cancellationToken.ThrowIfCancellationRequested(); - var config = new Config(modelDir); + using var config = new Config(modelDir); if (!string.IsNullOrEmpty(provider)) { config.AppendProvider(provider); } + chatClient?.Dispose(); chatClient = new OnnxRuntimeGenAIChatClient(config, true, options); cancellationToken.ThrowIfCancellationRequested(); }, diff --git a/AIDevGallery/Samples/SharedCode/IChatClient/PhiSilicaClient.cs b/AIDevGallery/Samples/SharedCode/IChatClient/PhiSilicaClient.cs index b734c70d..3453aa57 100644 --- a/AIDevGallery/Samples/SharedCode/IChatClient/PhiSilicaClient.cs +++ b/AIDevGallery/Samples/SharedCode/IChatClient/PhiSilicaClient.cs @@ -20,7 +20,7 @@ namespace AIDevGallery.Samples.SharedCode; -internal class WCRException : Exception +internal sealed class WCRException : Exception { public WCRException(string message) : base(message) @@ -28,7 +28,12 @@ public WCRException(string message) } } -internal class PhiSilicaClient : IChatClient +// Class is already sealed and all disposable members are properly disposed in Dispose() method +#pragma warning disable IDISP025 // Class with no virtual dispose method should be sealed +#pragma warning disable IDISP002 // Dispose member +internal sealed class PhiSilicaClient : IChatClient, IDisposable +#pragma warning restore IDISP002 +#pragma warning restore IDISP025 { // Search Options private const SeverityLevel DefaultInputModeration = SeverityLevel.Minimum; @@ -39,6 +44,7 @@ internal class PhiSilicaClient : IChatClient private LanguageModel _languageModel; private LanguageModelContext? _languageModelContext; + private bool _disposed; public ChatClientMetadata Metadata { get; } @@ -191,6 +197,7 @@ private string GetPrompt(IEnumerable history) var firstMessage = history.FirstOrDefault(); + _languageModelContext?.Dispose(); _languageModelContext = firstMessage?.Role == ChatRole.System ? _languageModel?.CreateContext(firstMessage.Text, new ContentFilterOptions()) : _languageModel?.CreateContext(); @@ -219,11 +226,6 @@ private string GetPrompt(IEnumerable history) return prompt; } - public void Dispose() - { - _languageModel.Dispose(); - } - public object? GetService(Type serviceType, object? serviceKey = null) { return serviceKey is not null ? null : @@ -286,4 +288,16 @@ public async IAsyncEnumerable GenerateStreamResponseAsync(string prompt, _ => string.Empty, }; } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _languageModelContext?.Dispose(); + _languageModel?.Dispose(); + _disposed = true; + } } \ No newline at end of file diff --git a/AIDevGallery/Samples/SharedCode/StableDiffusionCode/SafetyChecker.cs b/AIDevGallery/Samples/SharedCode/StableDiffusionCode/SafetyChecker.cs index 82d0493e..6dec34aa 100644 --- a/AIDevGallery/Samples/SharedCode/StableDiffusionCode/SafetyChecker.cs +++ b/AIDevGallery/Samples/SharedCode/StableDiffusionCode/SafetyChecker.cs @@ -49,7 +49,7 @@ private Task GetInferenceSession(string modelPath, ExecutionPr Debug.WriteLine($"WARNING: Failed to install packages: {ex.Message}"); } - SessionOptions sessionOptions = new(); + using SessionOptions sessionOptions = new(); sessionOptions.RegisterOrtExtensions(); sessionOptions.AddFreeDimensionOverrideByName("batch", 1); diff --git a/AIDevGallery/Samples/SharedCode/StableDiffusionCode/StableDiffusion.cs b/AIDevGallery/Samples/SharedCode/StableDiffusionCode/StableDiffusion.cs index b773c32f..1acda156 100644 --- a/AIDevGallery/Samples/SharedCode/StableDiffusionCode/StableDiffusion.cs +++ b/AIDevGallery/Samples/SharedCode/StableDiffusionCode/StableDiffusion.cs @@ -47,9 +47,13 @@ public async Task InitializeAsync(ExecutionProviderDevicePolicy? policy, string? { string tokenizerPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets", config.TokenizerModelPath); + textProcessor?.Dispose(); textProcessor = await TextProcessing.CreateAsync(config, tokenizerPath, config.TextEncoderModelPath, policy, epName, compileModel, deviceType); + unetInferenceSession?.Dispose(); unetInferenceSession = await GetInferenceSession(config.UnetModelPath, policy, epName, compileModel, deviceType); + vaeDecoder?.Dispose(); vaeDecoder = await VaeDecoder.CreateAsync(config, config.VaeDecoderModelPath, policy, epName, compileModel, deviceType); + safetyChecker?.Dispose(); safetyChecker = await SafetyChecker.CreateAsync(config.SafetyModelPath, policy, epName, compileModel, deviceType); } @@ -78,7 +82,7 @@ private Task GetInferenceSession(string modelPath, ExecutionPr Debug.WriteLine($"WARNING: Failed to install packages: {ex.Message}"); } - SessionOptions sessionOptions = new(); + using SessionOptions sessionOptions = new(); sessionOptions.RegisterOrtExtensions(); sessionOptions.AddFreeDimensionOverrideByName("batch", 2); diff --git a/AIDevGallery/Samples/SharedCode/StableDiffusionCode/TextProcessing.cs b/AIDevGallery/Samples/SharedCode/StableDiffusionCode/TextProcessing.cs index 13347f8f..610623ee 100644 --- a/AIDevGallery/Samples/SharedCode/StableDiffusionCode/TextProcessing.cs +++ b/AIDevGallery/Samples/SharedCode/StableDiffusionCode/TextProcessing.cs @@ -57,7 +57,7 @@ private Task GetInferenceSession(StableDiffusionConfig config, Debug.WriteLine($"WARNING: Failed to install packages: {ex.Message}"); } - SessionOptions sessionOptions = new(); + using SessionOptions sessionOptions = new(); sessionOptions.RegisterOrtExtensions(); sessionOptions.AddFreeDimensionOverrideByName("batch", 1); diff --git a/AIDevGallery/Samples/SharedCode/StableDiffusionCode/VaeDecoder.cs b/AIDevGallery/Samples/SharedCode/StableDiffusionCode/VaeDecoder.cs index 65409240..7aca6ef3 100644 --- a/AIDevGallery/Samples/SharedCode/StableDiffusionCode/VaeDecoder.cs +++ b/AIDevGallery/Samples/SharedCode/StableDiffusionCode/VaeDecoder.cs @@ -54,7 +54,7 @@ private Task GetInferenceSession(StableDiffusionConfig config, Debug.WriteLine($"WARNING: Failed to install packages: {ex.Message}"); } - SessionOptions sessionOptions = new(); + using SessionOptions sessionOptions = new(); sessionOptions.RegisterOrtExtensions(); sessionOptions.AddFreeDimensionOverrideByName("batch", 1); diff --git a/AIDevGallery/Samples/SharedCode/WhisperWrapper.cs b/AIDevGallery/Samples/SharedCode/WhisperWrapper.cs index c2b7e98e..896af12e 100644 --- a/AIDevGallery/Samples/SharedCode/WhisperWrapper.cs +++ b/AIDevGallery/Samples/SharedCode/WhisperWrapper.cs @@ -31,9 +31,7 @@ public static async Task CreateAsync(string modelPath, Execution Debug.WriteLine($"WARNING: Failed to install packages: {ex.Message}"); } -#pragma warning disable CA2000 // Dispose objects before losing scope - SessionOptions sessionOptions = new(); -#pragma warning restore CA2000 // Dispose objects before losing scope + using SessionOptions sessionOptions = new(); sessionOptions.RegisterOrtExtensions(); if (policy != null) diff --git a/AIDevGallery/Samples/WCRAPIs/BackgroundRemover.xaml.cs b/AIDevGallery/Samples/WCRAPIs/BackgroundRemover.xaml.cs index b617ceec..3d11e83c 100644 --- a/AIDevGallery/Samples/WCRAPIs/BackgroundRemover.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/BackgroundRemover.xaml.cs @@ -188,10 +188,10 @@ private async Task SetImageSource(Image image, SoftwareBitmap softwareBitmap) try { - var extractor = await ImageObjectExtractor.CreateWithSoftwareBitmapAsync(bitmap); + using var extractor = await ImageObjectExtractor.CreateWithSoftwareBitmapAsync(bitmap); try { - var mask = extractor.GetSoftwareBitmapObjectMask(new ImageObjectExtractorHint([], includePoints, [])); + using var mask = extractor.GetSoftwareBitmapObjectMask(new ImageObjectExtractorHint([], includePoints, [])); return ApplyMask(bitmap, mask); } catch (Exception ex) @@ -258,7 +258,7 @@ private async void RemoveBackground_Click(object sender, RoutedEventArgs e) return; } - var outputBitmap = await ExtractBackground(_inputBitmap, _selectionPoints); + using var outputBitmap = await ExtractBackground(_inputBitmap, _selectionPoints); if (outputBitmap != null) { _originalBitmap = _inputBitmap; diff --git a/AIDevGallery/Samples/WCRAPIs/ColoringBook.xaml.cs b/AIDevGallery/Samples/WCRAPIs/ColoringBook.xaml.cs index 930ea1b3..268413c8 100644 --- a/AIDevGallery/Samples/WCRAPIs/ColoringBook.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/ColoringBook.xaml.cs @@ -37,7 +37,7 @@ namespace AIDevGallery.Samples.WCRAPIs; "WinDev.png" ], Icon = "\uEE6F")] -internal sealed partial class ColoringBook : BaseSamplePage +internal sealed partial class ColoringBook : BaseSamplePage, IDisposable { private const int MaxLength = 1000; private const int FloodFillTolerance = 25; @@ -53,6 +53,18 @@ public ColoringBook() this.InitializeComponent(); } + public void Dispose() + { + while (_bitmaps.Count > 0) + { + _bitmaps.Pop()?.Dispose(); + } + + _inputBitmap?.Dispose(); + _generator?.Dispose(); + _cts?.Dispose(); + } + protected override async Task LoadModelAsync(SampleNavigationParameters sampleParams) { var readyState = ImageGenerator.GetReadyState(); @@ -155,6 +167,7 @@ private static bool IsImageFile(string fileName) private async Task SetImage(IRandomAccessStream stream) { var decoder = await BitmapDecoder.CreateAsync(stream); + _inputBitmap?.Dispose(); _inputBitmap = await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); if (_inputBitmap == null) @@ -196,6 +209,7 @@ private async Task ChangeImage() StopBtn.Visibility = Visibility.Visible; IsProgressVisible = true; InputTextBox.IsEnabled = false; + _cts?.Dispose(); _cts = new CancellationTokenSource(); var prompt = InputTextBox.Text; @@ -209,18 +223,26 @@ await Task.Run( return; } - var outputBitmap = ApplyMaskWithPrompt(prompt, _inputBitmap, mask); - if (_cts!.Token.IsCancellationRequested) + using (mask) + { + // Ownership of outputBitmap is transferred to _inputBitmap, so it should not be disposed here +#pragma warning disable IDISP001 // Dispose created +#pragma warning disable IDISP003 // Dispose previous before re-assigning + var outputBitmap = ApplyMaskWithPrompt(prompt, _inputBitmap, mask); + if (_cts!.Token.IsCancellationRequested) { return; } - if (outputBitmap != null) - { - SetImageSource(outputBitmap); - _bitmaps.Push(_inputBitmap); - _inputBitmap = outputBitmap; - SwitchInputOutputView(); + if (outputBitmap != null) + { + SetImageSource(outputBitmap); + _bitmaps.Push(_inputBitmap); + _inputBitmap = outputBitmap; + SwitchInputOutputView(); + } +#pragma warning restore IDISP003 +#pragma warning restore IDISP001 } }, _cts.Token); @@ -281,6 +303,7 @@ private void RevertButton_Click(object sender, RoutedEventArgs e) { if (_bitmaps.Count > 0) { + _inputBitmap?.Dispose(); _inputBitmap = _bitmaps.Pop(); SetImageSource(_inputBitmap); } diff --git a/AIDevGallery/Samples/WCRAPIs/DescribeYourChange.xaml.cs b/AIDevGallery/Samples/WCRAPIs/DescribeYourChange.xaml.cs index 1f0e44af..8daaed9f 100644 --- a/AIDevGallery/Samples/WCRAPIs/DescribeYourChange.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/DescribeYourChange.xaml.cs @@ -27,7 +27,7 @@ namespace AIDevGallery.Samples.WCRAPIs; "Microsoft.Extensions.AI" ], Icon = "\uEF15")] -internal sealed partial class DescribeYourChange : BaseSamplePage +internal sealed partial class DescribeYourChange : BaseSamplePage, IDisposable { private const int MaxLength = 5000; private bool _isProgressVisible; @@ -113,6 +113,11 @@ private void CleanUp() _languageModel?.Dispose(); } + public void Dispose() + { + CleanUp(); + } + public bool IsProgressVisible { get => _isProgressVisible; @@ -174,6 +179,7 @@ public async Task RewriteTextCustom(string prompt) IsProgressVisible = true; _cts?.Cancel(); + _cts?.Dispose(); _cts = new CancellationTokenSource(); // diff --git a/AIDevGallery/Samples/WCRAPIs/ForegroundExtractor.xaml.cs b/AIDevGallery/Samples/WCRAPIs/ForegroundExtractor.xaml.cs index cf80e5f8..99b2153c 100644 --- a/AIDevGallery/Samples/WCRAPIs/ForegroundExtractor.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/ForegroundExtractor.xaml.cs @@ -29,7 +29,7 @@ namespace AIDevGallery.Samples.WCRAPIs; "horse.png" ], Icon = "\uEE6F")] -internal sealed partial class ForegroundExtractor : BaseSamplePage +internal sealed partial class ForegroundExtractor : BaseSamplePage, IDisposable { private ImageForegroundExtractor? _foregroundExtractor; private SoftwareBitmap? _inputBitmap; @@ -40,6 +40,13 @@ public ForegroundExtractor() this.InitializeComponent(); } + public void Dispose() + { + _foregroundExtractor?.Dispose(); + _inputBitmap?.Dispose(); + _outputBitmap?.Dispose(); + } + protected override async Task LoadModelAsync(SampleNavigationParameters sampleParams) { var readyState = ImageForegroundExtractor.GetReadyState(); @@ -88,6 +95,7 @@ private async Task SetInputAndGeneratedOutput() SaveButton.Visibility = Visibility.Collapsed; await SetImage(InputImage, _inputBitmap); GeneratedImage.Source = null; + _outputBitmap?.Dispose(); _outputBitmap = await Task.Run(() => GetForeground(_inputBitmap)); if (_outputBitmap != null) { @@ -173,11 +181,6 @@ private async Task SetImage(Image image, SoftwareBitmap? bitmap) return; } - if (image.Source is SoftwareBitmapSource previousSource) - { - previousSource.Dispose(); - } - var convertedBitmap = SoftwareBitmap.Convert(bitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); var bitmapSource = new SoftwareBitmapSource(); @@ -194,7 +197,7 @@ private async Task SetImage(Image image, SoftwareBitmap? bitmap) try { - var mask = _foregroundExtractor.GetMaskFromSoftwareBitmap(bitmap); + using var mask = _foregroundExtractor.GetMaskFromSoftwareBitmap(bitmap); return ApplyMask(bitmap, mask); } catch (Exception ex) diff --git a/AIDevGallery/Samples/WCRAPIs/ImageDescription.xaml.cs b/AIDevGallery/Samples/WCRAPIs/ImageDescription.xaml.cs index c5ecf416..cf1f8098 100644 --- a/AIDevGallery/Samples/WCRAPIs/ImageDescription.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/ImageDescription.xaml.cs @@ -35,7 +35,7 @@ namespace AIDevGallery.Samples.WCRAPIs; ], Icon = "\uEE6F")] -internal sealed partial class ImageDescription : BaseSamplePage +internal sealed partial class ImageDescription : BaseSamplePage, IDisposable { private readonly Dictionary _descriptionKindDictionary = new Dictionary { @@ -55,6 +55,12 @@ public ImageDescription() this.InitializeComponent(); } + public void Dispose() + { + _imageDescriptor?.Dispose(); + _cts?.Dispose(); + } + protected override async Task LoadModelAsync(SampleNavigationParameters sampleParams) { var readyState = ImageDescriptionGenerator.GetReadyState(); @@ -174,6 +180,7 @@ private async Task SetImage(IRandomAccessStream stream) private async Task DescribeImage(SoftwareBitmap bitmap, ImageDescriptionKind descriptionKind) { _cts?.Cancel(); + _cts?.Dispose(); _cts = new CancellationTokenSource(); DispatcherQueue?.TryEnqueue(() => { diff --git a/AIDevGallery/Samples/WCRAPIs/IncreaseFidelity.xaml.cs b/AIDevGallery/Samples/WCRAPIs/IncreaseFidelity.xaml.cs index cbacab3f..c99a06bb 100644 --- a/AIDevGallery/Samples/WCRAPIs/IncreaseFidelity.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/IncreaseFidelity.xaml.cs @@ -183,13 +183,16 @@ private async void ScaleImage() var newWidth = (int)(_originalImage.PixelWidth * ScaleSlider.Value); var newHeight = (int)(_originalImage.PixelHeight * ScaleSlider.Value); - SoftwareBitmap? bitmap; + SoftwareBitmap? bitmap = null; try { + // Ownership of bitmap is transferred to another variable, so it should not be disposed here +#pragma warning disable IDISP001 // Dispose created bitmap = await Task.Run(() => { return _imageScaler.ScaleSoftwareBitmap(_originalImage, newWidth, newHeight); }); +#pragma warning restore IDISP001 } catch (Exception ex) { diff --git a/AIDevGallery/Samples/WCRAPIs/KnowledgeRetrieval.xaml.cs b/AIDevGallery/Samples/WCRAPIs/KnowledgeRetrieval.xaml.cs index a3eef942..9bac2295 100644 --- a/AIDevGallery/Samples/WCRAPIs/KnowledgeRetrieval.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/KnowledgeRetrieval.xaml.cs @@ -41,7 +41,7 @@ namespace AIDevGallery.Samples.WCRAPIs; ], Icon = "\uEE6F")] -internal sealed partial class KnowledgeRetrieval : BaseSamplePage +internal sealed partial class KnowledgeRetrieval : BaseSamplePage, IDisposable { private ObservableCollection TextDataItems { get; } = new(); public ObservableCollection Messages { get; } = []; @@ -58,7 +58,7 @@ internal sealed partial class KnowledgeRetrieval : BaseSamplePage // This is some text data that we want to add to the index: private Dictionary simpleTextData = new Dictionary { - { "item1", "Preparing a hearty vegetable stew begins with chopping fresh carrots, onions, and celery. Saut them in olive oil until fragrant, then add diced tomatoes, herbs, and vegetable broth. Simmer gently for an hour, allowing flavors to meld into a comforting dish perfect for cold evenings." }, + { "item1", "Preparing a hearty vegetable stew begins with chopping fresh carrots, onions, and celery. Saut� them in olive oil until fragrant, then add diced tomatoes, herbs, and vegetable broth. Simmer gently for an hour, allowing flavors to meld into a comforting dish perfect for cold evenings." }, { "item2", "Modern exhibition design combines narrative flow with spatial strategy. Lighting emphasizes focal objects while circulation paths avoid bottlenecks. Materials complement artifacts without visual competition. Interactive elements invite engagement but remain intuitive. Environmental controls protect sensitive works. Success balances scholarship, aesthetics, and visitor experience through thoughtful, cohesive design choices." }, { "item3", "Domestic cats communicate through posture, tail flicks, and vocalizations. Play mimics hunting behaviors like stalking and pouncing, supporting agility and mental stimulation. Scratching maintains claws and marks territory, so provide sturdy posts. Balanced diets, hydration, and routine veterinary care sustain health. Safe retreats and vertical spaces reduce stress and encourage exploration." }, { "item4", "Snowboarding across pristine slopes combines agility, balance, and speed. Riders carve smooth turns on powder, adjust stance for control, and master jumps in terrain parks. Essential gear includes boots, bindings, and helmets for safety. Embrace crisp alpine air while perfecting tricks and enjoying the thrill of winter adventure." }, @@ -97,6 +97,7 @@ await Task.Run(async () => winMlSampleOptions: null, loadingCanceledToken: CancellationToken.None); + _model?.Dispose(); _model = await ragSampleParams.GetIChatClientAsync(); } catch (Exception ex) @@ -153,6 +154,11 @@ private void CleanUp() _indexer = null; } + public void Dispose() + { + CleanUp(); + } + private void CancelResponse() { StopBtn.Visibility = Visibility.Collapsed; @@ -293,6 +299,7 @@ private void AddMessage(string text) InputBox.IsEnabled = false; }); + cts?.Dispose(); cts = new CancellationTokenSource(); // Use AppContentIndexer query here. diff --git a/AIDevGallery/Samples/WCRAPIs/MagicEraser.xaml.cs b/AIDevGallery/Samples/WCRAPIs/MagicEraser.xaml.cs index 91990443..5542811f 100644 --- a/AIDevGallery/Samples/WCRAPIs/MagicEraser.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/MagicEraser.xaml.cs @@ -178,7 +178,10 @@ private async void EraseObject_Click(object sender, RoutedEventArgs e) try { + // Ownership of outputBitmap is transferred to _inputBitmap, so it should not be disposed here +#pragma warning disable IDISP001 // Dispose created var outputBitmap = _eraser.RemoveFromSoftwareBitmap(_inputBitmap, _maskBitmap); +#pragma warning restore IDISP001 if (outputBitmap != null) { _bitmaps.Push(_inputBitmap); diff --git a/AIDevGallery/Samples/WCRAPIs/PhiSilicaBasic.xaml.cs b/AIDevGallery/Samples/WCRAPIs/PhiSilicaBasic.xaml.cs index efd73fa6..ffccd4a8 100644 --- a/AIDevGallery/Samples/WCRAPIs/PhiSilicaBasic.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/PhiSilicaBasic.xaml.cs @@ -30,7 +30,7 @@ namespace AIDevGallery.Samples.WCRAPIs; "Microsoft.Extensions.AI" ], Icon = "\uEE6F")] -internal sealed partial class PhiSilicaBasic : BaseSamplePage +internal sealed partial class PhiSilicaBasic : BaseSamplePage, IDisposable { private const int MaxLength = 1000; private bool _isProgressVisible; @@ -107,6 +107,11 @@ private void CleanUp() _languageModel?.Dispose(); } + public void Dispose() + { + CleanUp(); + } + public bool IsProgressVisible { get => _isProgressVisible; @@ -136,6 +141,7 @@ public async Task GenerateText(string prompt) _languageModel ??= await LanguageModel.CreateAsync(); _cts?.Cancel(); + _cts?.Dispose(); _cts = new CancellationTokenSource(); // diff --git a/AIDevGallery/Samples/WCRAPIs/PhiSilicaLoRa.xaml.cs b/AIDevGallery/Samples/WCRAPIs/PhiSilicaLoRa.xaml.cs index fc5fef1c..af1dc4b6 100644 --- a/AIDevGallery/Samples/WCRAPIs/PhiSilicaLoRa.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/PhiSilicaLoRa.xaml.cs @@ -33,7 +33,7 @@ namespace AIDevGallery.Samples.WCRAPIs; ], Icon = "\uEE6F")] -internal sealed partial class PhiSilicaLoRa : BaseSamplePage +internal sealed partial class PhiSilicaLoRa : BaseSamplePage, IDisposable { internal enum GenerationType { @@ -98,6 +98,7 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa } _languageModel = await LanguageModel.CreateAsync(); + _loraModel?.Dispose(); _loraModel = new LanguageModelExperimental(_languageModel); } else @@ -144,6 +145,13 @@ private async void CleanUp() _languageModel?.Dispose(); } + public void Dispose() + { + _languageModel?.Dispose(); + _loraModel?.Dispose(); + _cts?.Dispose(); + } + public bool IsProgressVisible { get => _isProgressVisible; @@ -174,7 +182,7 @@ public async Task GenerateText(string prompt, string systemPrompt, TextBlock tex // the context has the system prompt and history // it is created for each query to avoid bringing history from previous queries - LanguageModelContext? context = systemPrompt.Length > 0 ? _languageModel.CreateContext(systemPrompt) : null; + using LanguageModelContext? context = systemPrompt.Length > 0 ? _languageModel.CreateContext(systemPrompt) : null; operation = context == null ? options == null ? _languageModel.GenerateResponseAsync(prompt) : _loraModel.GenerateResponseAsync(prompt, options) : options == null ? _languageModel.GenerateResponseAsync(context, prompt, new LanguageModelOptions()) : _loraModel.GenerateResponseAsync(context, prompt, options); @@ -265,6 +273,7 @@ private async Task RunQuery() IsProgressVisible = true; _cts?.Cancel(); + _cts?.Dispose(); _cts = new CancellationTokenSource(); var options = new LanguageModelOptionsExperimental(); try diff --git a/AIDevGallery/Samples/WCRAPIs/RestyleImage.xaml.cs b/AIDevGallery/Samples/WCRAPIs/RestyleImage.xaml.cs index ffffa21c..04816961 100644 --- a/AIDevGallery/Samples/WCRAPIs/RestyleImage.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/RestyleImage.xaml.cs @@ -38,7 +38,7 @@ namespace AIDevGallery.Samples.WCRAPIs; "Microsoft.Extensions.AI" ], Icon = "\uEE6F")] -internal sealed partial class RestyleImage : BaseSamplePage +internal sealed partial class RestyleImage : BaseSamplePage, IDisposable { private const int MaxLength = 1000; private bool _isProgressVisible; @@ -96,6 +96,11 @@ private void CleanUp() _imageModel?.Dispose(); } + public void Dispose() + { + CleanUp(); + } + public bool IsProgressVisible { get => _isProgressVisible; @@ -140,6 +145,7 @@ public async Task GenerateImage(string prompt) using var inputBuffer = ImageBuffer.CreateForSoftwareBitmap(_inputBitmap); SendSampleInteractedEvent("GenerateImage"); // + _cts?.Dispose(); _cts = new CancellationTokenSource(); var result = await Task.Run( @@ -171,8 +177,8 @@ public async Task GenerateImage(string prompt) return; } - var softwareBitmap = result.Image.CopyToSoftwareBitmap(); - var convertedImage = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); + using var softwareBitmap = result.Image.CopyToSoftwareBitmap(); + using var convertedImage = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); if (convertedImage != null) { var source = new SoftwareBitmapSource(); diff --git a/AIDevGallery/Samples/WCRAPIs/SDXL.xaml.cs b/AIDevGallery/Samples/WCRAPIs/SDXL.xaml.cs index 46012129..2e1ded0d 100644 --- a/AIDevGallery/Samples/WCRAPIs/SDXL.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/SDXL.xaml.cs @@ -36,7 +36,7 @@ namespace AIDevGallery.Samples.WCRAPIs; "Microsoft.Extensions.AI" ], Icon = "\uEE6F")] -internal sealed partial class SDXL : BaseSamplePage +internal sealed partial class SDXL : BaseSamplePage, IDisposable { private const int MaxLength = 1000; private bool _isProgressVisible; @@ -92,6 +92,11 @@ private void CleanUp() _generator?.Dispose(); } + public void Dispose() + { + CleanUp(); + } + public bool IsProgressVisible { get => _isProgressVisible; @@ -123,6 +128,7 @@ public async Task GenerateImage(string prompt) SendSampleInteractedEvent("GenerateImage"); // + _cts?.Dispose(); _cts = new CancellationTokenSource(); ImageGeneratorResult? result = null; @@ -155,8 +161,8 @@ public async Task GenerateImage(string prompt) return; } - var softwareBitmap = result.Image.CopyToSoftwareBitmap(); - var convertedImage = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); + using var softwareBitmap = result.Image.CopyToSoftwareBitmap(); + using var convertedImage = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); if (convertedImage != null) { var source = new SoftwareBitmapSource(); diff --git a/AIDevGallery/Samples/WCRAPIs/TextRewrite.xaml.cs b/AIDevGallery/Samples/WCRAPIs/TextRewrite.xaml.cs index 56efc728..721132ac 100644 --- a/AIDevGallery/Samples/WCRAPIs/TextRewrite.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/TextRewrite.xaml.cs @@ -30,7 +30,7 @@ namespace AIDevGallery.Samples.WCRAPIs; "Microsoft.Extensions.AI" ], Icon = "\uEE56")] -internal sealed partial class TextRewrite : BaseSamplePage +internal sealed partial class TextRewrite : BaseSamplePage, IDisposable { private const int MaxLength = 5000; private bool _isProgressVisible; @@ -115,6 +115,11 @@ private void CleanUp() _languageModel?.Dispose(); } + public void Dispose() + { + CleanUp(); + } + public bool IsProgressVisible { get => _isProgressVisible; @@ -148,6 +153,7 @@ public async Task RewriteText(string prompt) IsProgressVisible = true; _cts?.Cancel(); + _cts?.Dispose(); _cts = new CancellationTokenSource(); // diff --git a/AIDevGallery/Samples/WCRAPIs/TextSummarize.xaml.cs b/AIDevGallery/Samples/WCRAPIs/TextSummarize.xaml.cs index 360f7681..667b6161 100644 --- a/AIDevGallery/Samples/WCRAPIs/TextSummarize.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/TextSummarize.xaml.cs @@ -30,7 +30,7 @@ namespace AIDevGallery.Samples.WCRAPIs; "Microsoft.Extensions.AI" ], Icon = "\uE8D4")] -internal sealed partial class TextSummarize : BaseSamplePage +internal sealed partial class TextSummarize : BaseSamplePage, IDisposable { private const int MaxLength = 5000; private bool _isProgressVisible; @@ -115,6 +115,11 @@ private void CleanUp() _languageModel?.Dispose(); } + public void Dispose() + { + CleanUp(); + } + public bool IsProgressVisible { get => _isProgressVisible; @@ -148,6 +153,7 @@ public async Task SummarizeText(string prompt) IsProgressVisible = true; _cts?.Cancel(); + _cts?.Dispose(); _cts = new CancellationTokenSource(); // diff --git a/AIDevGallery/Samples/WCRAPIs/TextToTable.xaml.cs b/AIDevGallery/Samples/WCRAPIs/TextToTable.xaml.cs index ef7b3ba9..460ef509 100644 --- a/AIDevGallery/Samples/WCRAPIs/TextToTable.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/TextToTable.xaml.cs @@ -31,7 +31,7 @@ namespace AIDevGallery.Samples.WCRAPIs; "Microsoft.Extensions.AI" ], Icon = "\uEE56")] -internal sealed partial class TextToTable : BaseSamplePage +internal sealed partial class TextToTable : BaseSamplePage, IDisposable { private const int MaxLength = 5000; private bool _isProgressVisible; @@ -116,6 +116,11 @@ private void CleanUp() _languageModel?.Dispose(); } + public void Dispose() + { + CleanUp(); + } + public bool IsProgressVisible { get => _isProgressVisible; @@ -149,6 +154,7 @@ public async Task ConvertText(string prompt) IsProgressVisible = true; _cts?.Cancel(); + _cts?.Dispose(); _cts = new CancellationTokenSource(); var result = await _textToTableConverter.ConvertAsync(prompt).AsTask(_cts.Token); diff --git a/AIDevGallery/Utils/GithubApi.cs b/AIDevGallery/Utils/GithubApi.cs index 50f26813..1fbec2fc 100644 --- a/AIDevGallery/Utils/GithubApi.cs +++ b/AIDevGallery/Utils/GithubApi.cs @@ -8,6 +8,7 @@ namespace AIDevGallery.Utils; internal class GithubApi { + private static readonly HttpClient _httpClient = new(); private static readonly string RawGhUrl = "https://raw.githubusercontent.com"; /// @@ -21,8 +22,7 @@ public static async Task GetContentsOfTextFile(string fileUrl) var requestUrl = $"{RawGhUrl}/{url.Organization}/{url.Repo}/{url.Ref}/{url.Path}"; - using var client = new HttpClient(); - var response = await client.GetAsync(requestUrl); + using var response = await _httpClient.GetAsync(requestUrl); return await response.Content.ReadAsStringAsync(); } } \ No newline at end of file diff --git a/AIDevGallery/Utils/HuggingFaceApi.cs b/AIDevGallery/Utils/HuggingFaceApi.cs index 707221a2..43296248 100644 --- a/AIDevGallery/Utils/HuggingFaceApi.cs +++ b/AIDevGallery/Utils/HuggingFaceApi.cs @@ -15,6 +15,7 @@ namespace AIDevGallery.Utils; /// internal class HuggingFaceApi { + private static readonly HttpClient _httpClient = new(); private static readonly string HFApiUrl = "https://huggingface.co/api"; private static readonly string HFUrl = "https://huggingface.co"; @@ -27,8 +28,7 @@ internal class HuggingFaceApi public static async Task?> FindModels(string query, string filter = "onnx") { string searchUrl = $"{HFApiUrl}/models?search={query}&filter={filter}&full=true&config=true"; - using var client = new HttpClient(); - var response = await client.GetAsync(searchUrl); + using var response = await _httpClient.GetAsync(searchUrl); var responseContent = await response.Content.ReadAsStringAsync(); try @@ -53,8 +53,7 @@ public static async Task GetContentsOfTextFile(string modelId, string fi { var url = $"{HFUrl}/{modelId}/resolve/{commitOrBranch}/{filePath}"; - using var client = new HttpClient(); - var response = await client.GetAsync(url); + using var response = await _httpClient.GetAsync(url); return await response.Content.ReadAsStringAsync(); } @@ -69,8 +68,7 @@ public static async Task GetContentsOfTextFile(string fileUrl) var requestUrl = $"{HFUrl}/{url.Organization}/{url.Repo}/resolve/{url.Ref}/{url.Path}"; - using var client = new HttpClient(); - var response = await client.GetAsync(requestUrl); + using var response = await _httpClient.GetAsync(requestUrl); return await response.Content.ReadAsStringAsync(); } } \ No newline at end of file diff --git a/AIDevGallery/Utils/ModelDownload.cs b/AIDevGallery/Utils/ModelDownload.cs index 454b37ab..b48d8191 100644 --- a/AIDevGallery/Utils/ModelDownload.cs +++ b/AIDevGallery/Utils/ModelDownload.cs @@ -88,7 +88,16 @@ protected set public void Dispose() { - CancellationTokenSource.Dispose(); + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + CancellationTokenSource.Dispose(); + } } public ModelDownload(ModelDetails details) @@ -103,7 +112,7 @@ public ModelDownload(ModelDetails details) public abstract void CancelDownload(); } -internal class OnnxModelDownload : ModelDownload +internal sealed class OnnxModelDownload : ModelDownload { public ModelUrl ModelUrl { get; set; } @@ -328,7 +337,7 @@ private static async Task ComputeSha256Async(string filePath, Cancellati } } -internal class FoundryLocalModelDownload : ModelDownload +internal sealed class FoundryLocalModelDownload : ModelDownload { public FoundryLocalModelDownload(ModelDetails details) : base(details) diff --git a/AIDevGallery/Utils/ModelDownloadQueue.cs b/AIDevGallery/Utils/ModelDownloadQueue.cs index d07430e8..94e254f6 100644 --- a/AIDevGallery/Utils/ModelDownloadQueue.cs +++ b/AIDevGallery/Utils/ModelDownloadQueue.cs @@ -88,7 +88,6 @@ public void CancelModelDownload(ModelDownload download) ModelDownloadCancelEvent.Log(download.Details.Url); _queue.Remove(download); ModelsChanged?.Invoke(this); - download.Dispose(); } public IReadOnlyList GetDownloads() diff --git a/AIDevGallery/ViewModels/DownloadableModel.cs b/AIDevGallery/ViewModels/DownloadableModel.cs index e4044a8c..2364cca6 100644 --- a/AIDevGallery/ViewModels/DownloadableModel.cs +++ b/AIDevGallery/ViewModels/DownloadableModel.cs @@ -27,6 +27,7 @@ internal partial class DownloadableModel : BaseModel public bool IsDownloadEnabled => Compatibility.CompatibilityState != ModelCompatibilityState.NotCompatible; +#pragma warning disable IDISP008 // Don't assign member with injected and created disposables - ModelDownload lifecycle is managed by ModelDownloadQueue private ModelDownload? _modelDownload; public ModelDownload? ModelDownload @@ -59,6 +60,7 @@ public ModelDownload? ModelDownload CanDownload = false; } } +#pragma warning restore IDISP008 private DownloadableModel(ModelDetails modelDetails, ModelDownload? modelDownload) : base(modelDetails) From 9d0f064c4e6511f717ae474a3cbbd36b23786b3d Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Mon, 5 Jan 2026 17:35:40 +0800 Subject: [PATCH 02/23] fix --- .../HomePage/Header/Lights/HoverLight.cs | 20 ++-- AIDevGallery/Controls/OpacityMask.xaml.cs | 14 +-- AIDevGallery/Controls/Shimmer.xaml.cs | 101 ++++++++---------- .../SharedCode/IChatClient/PhiSilicaClient.cs | 5 - 4 files changed, 54 insertions(+), 86 deletions(-) diff --git a/AIDevGallery/Controls/HomePage/Header/Lights/HoverLight.cs b/AIDevGallery/Controls/HomePage/Header/Lights/HoverLight.cs index 801dec22..464d52a8 100644 --- a/AIDevGallery/Controls/HomePage/Header/Lights/HoverLight.cs +++ b/AIDevGallery/Controls/HomePage/Header/Lights/HoverLight.cs @@ -16,19 +16,16 @@ internal sealed partial class HoverLight : XamlLight, IDisposable private ExpressionAnimation? _lightPositionExpression; private Vector3KeyFrameAnimation? _offsetAnimation; private SpotLight? _spotLight; - private Compositor? _compositor; - private CompositionPropertySet? _hoverPosition; private static readonly string Id = typeof(HoverLight).FullName!; private bool _disposed; protected override void OnConnected(UIElement targetElement) { - _compositor?.Dispose(); - _compositor = CompositionTarget.GetCompositorForCurrentThread(); + Compositor compositor = CompositionTarget.GetCompositorForCurrentThread(); // Create SpotLight and set its properties _spotLight?.Dispose(); - _spotLight = _compositor.CreateSpotLight(); + _spotLight = compositor.CreateSpotLight(); _spotLight.InnerConeAngleInDegrees = 50f; _spotLight.InnerConeColor = Colors.FloralWhite; _spotLight.OuterConeAngleInDegrees = 20f; @@ -44,10 +41,10 @@ protected override void OnConnected(UIElement targetElement) // Define resting position Animation Vector3 restingPosition = new(200, 200, 400); - using (CubicBezierEasingFunction cbEasing = _compositor.CreateCubicBezierEasingFunction(new Vector2(0.3f, 0.7f), new Vector2(0.9f, 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 = compositor.CreateVector3KeyFrameAnimation(); _offsetAnimation.InsertKeyFrame(1, restingPosition, cbEasing); _offsetAnimation.Duration = TimeSpan.FromSeconds(0.5f); } @@ -55,11 +52,10 @@ protected override void OnConnected(UIElement targetElement) _spotLight.Offset = restingPosition; // Define expression animation that relates light's offset to pointer position - _hoverPosition?.Dispose(); - _hoverPosition = ElementCompositionPreview.GetPointerPositionPropertySet(targetElement); + CompositionPropertySet hoverPosition = ElementCompositionPreview.GetPointerPositionPropertySet(targetElement); _lightPositionExpression?.Dispose(); - _lightPositionExpression = _compositor.CreateExpressionAnimation("Vector3(hover.Position.X, hover.Position.Y, height)"); - _lightPositionExpression.SetReferenceParameter("hover", _hoverPosition); + _lightPositionExpression = compositor.CreateExpressionAnimation("Vector3(hover.Position.X, hover.Position.Y, height)"); + _lightPositionExpression.SetReferenceParameter("hover", hoverPosition); _lightPositionExpression.SetScalarParameter("height", 100.0f); // Configure pointer entered/ exited events @@ -131,8 +127,6 @@ public void Dispose() _lightPositionExpression?.Dispose(); _offsetAnimation?.Dispose(); _spotLight?.Dispose(); - _compositor?.Dispose(); - _hoverPosition?.Dispose(); _disposed = true; } } \ No newline at end of file diff --git a/AIDevGallery/Controls/OpacityMask.xaml.cs b/AIDevGallery/Controls/OpacityMask.xaml.cs index 87d26411..213c312b 100644 --- a/AIDevGallery/Controls/OpacityMask.xaml.cs +++ b/AIDevGallery/Controls/OpacityMask.xaml.cs @@ -6,7 +6,6 @@ using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Hosting; using Microsoft.UI.Xaml.Media; -using System; using System.Numerics; namespace AIDevGallery.Controls; @@ -17,7 +16,7 @@ namespace AIDevGallery.Controls; [TemplatePart(Name = RootGridTemplateName, Type = typeof(Grid))] [TemplatePart(Name = MaskContainerTemplateName, Type = typeof(Border))] [TemplatePart(Name = ContentPresenterTemplateName, Type = typeof(ContentPresenter))] -public sealed partial class OpacityMaskView : ContentControl, IDisposable +public partial class OpacityMaskView : ContentControl, IDisposable { // This is from Windows Community Toolkit Labs: https://github.com/CommunityToolkit/Labs-Windows/pull/491 @@ -35,7 +34,6 @@ public sealed partial class OpacityMaskView : ContentControl, IDisposable private CompositionBrush? _mask; private CompositionMaskBrush? _maskBrush; private SpriteVisual? _redirectVisual; - private bool _disposed; /// /// Initializes a new instance of the class. @@ -109,20 +107,10 @@ private static void OnOpacityMaskChanged(DependencyObject d, DependencyPropertyC maskBrush.Mask = opacityMask is null ? null : self._mask; } - /// - /// Disposes the composition resources used by this control. - /// public void Dispose() { - if (_disposed) - { - return; - } - _mask?.Dispose(); _maskBrush?.Dispose(); _redirectVisual?.Dispose(); - _compositor?.Dispose(); - _disposed = true; } } \ No newline at end of file diff --git a/AIDevGallery/Controls/Shimmer.xaml.cs b/AIDevGallery/Controls/Shimmer.xaml.cs index e589b2c8..5bd6c248 100644 --- a/AIDevGallery/Controls/Shimmer.xaml.cs +++ b/AIDevGallery/Controls/Shimmer.xaml.cs @@ -130,34 +130,32 @@ private bool TryInitializationResource() return false; } - using (var shapeVisual = _shape.GetVisual()) - { - var compositor = shapeVisual.Compositor; - - _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); - _spriteShape?.Dispose(); - _spriteShape = compositor.CreateSpriteShape(_rectangleGeometry); - _spriteShape.FillBrush = _shimmerMaskGradient; - _shapeVisual.Shapes.Clear(); - _shapeVisual.Shapes.Add(_spriteShape); - ElementCompositionPreview.SetElementChildVisual(_shape, _shapeVisual); - } + var shapeVisual = _shape.GetVisual(); + var compositor = shapeVisual.Compositor; + + _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); + _spriteShape?.Dispose(); + _spriteShape = compositor.CreateSpriteShape(_rectangleGeometry); + _spriteShape.FillBrush = _shimmerMaskGradient; + _shapeVisual.Shapes.Clear(); + _shapeVisual.Shapes.Add(_spriteShape); + ElementCompositionPreview.SetElementChildVisual(_shape, _shapeVisual); _initialized = true; return true; @@ -206,33 +204,26 @@ private void TryStartAnimation() return; } - using (var rootVisual = _shape.GetVisual()) - { - using (var reference = rootVisual.GetReference()) - { - _sizeAnimation?.Dispose(); - _sizeAnimation = reference.Size; - } - - _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; - _gradientStartPointAnimation.InsertKeyFrame(0.0f, new Vector2(InitialStartPointX, 0.0f)); - _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; - _gradientEndPointAnimation.InsertKeyFrame(0.0f, new Vector2(1.0f, 0.0f)); - _gradientEndPointAnimation.InsertKeyFrame(1.0f, new Vector2(-InitialStartPointX, 1.0f)); - _shimmerMaskGradient.StartAnimation(nameof(CompositionLinearGradientBrush.EndPoint), _gradientEndPointAnimation); - } + var rootVisual = _shape.GetVisual(); + _sizeAnimation = rootVisual.GetReference().Size; + _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; + _gradientStartPointAnimation.InsertKeyFrame(0.0f, new Vector2(InitialStartPointX, 0.0f)); + _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; + _gradientEndPointAnimation.InsertKeyFrame(0.0f, new Vector2(1.0f, 0.0f)); + _gradientEndPointAnimation.InsertKeyFrame(1.0f, new Vector2(-InitialStartPointX, 1.0f)); + _shimmerMaskGradient.StartAnimation(nameof(CompositionLinearGradientBrush.EndPoint), _gradientEndPointAnimation); _animationStarted = true; } diff --git a/AIDevGallery/Samples/SharedCode/IChatClient/PhiSilicaClient.cs b/AIDevGallery/Samples/SharedCode/IChatClient/PhiSilicaClient.cs index 3453aa57..0877b235 100644 --- a/AIDevGallery/Samples/SharedCode/IChatClient/PhiSilicaClient.cs +++ b/AIDevGallery/Samples/SharedCode/IChatClient/PhiSilicaClient.cs @@ -28,12 +28,7 @@ public WCRException(string message) } } -// Class is already sealed and all disposable members are properly disposed in Dispose() method -#pragma warning disable IDISP025 // Class with no virtual dispose method should be sealed -#pragma warning disable IDISP002 // Dispose member internal sealed class PhiSilicaClient : IChatClient, IDisposable -#pragma warning restore IDISP002 -#pragma warning restore IDISP025 { // Search Options private const SeverityLevel DefaultInputModeration = SeverityLevel.Minimum; From bd36d42689745e8b8d5e20c0d2a755cd7613edaf Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Mon, 5 Jan 2026 18:04:28 +0800 Subject: [PATCH 03/23] fix --- .../HomePage/Header/Lights/HoverLight.cs | 14 ++++---- AIDevGallery/Controls/OpacityMask.xaml.cs | 35 ++++++++++++++----- AIDevGallery/Controls/Shimmer.xaml.cs | 18 ++++++++-- 3 files changed, 51 insertions(+), 16 deletions(-) diff --git a/AIDevGallery/Controls/HomePage/Header/Lights/HoverLight.cs b/AIDevGallery/Controls/HomePage/Header/Lights/HoverLight.cs index 464d52a8..62e95b94 100644 --- a/AIDevGallery/Controls/HomePage/Header/Lights/HoverLight.cs +++ b/AIDevGallery/Controls/HomePage/Header/Lights/HoverLight.cs @@ -16,12 +16,15 @@ 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?.Dispose(); @@ -33,9 +36,6 @@ protected override void OnConnected(UIElement targetElement) _spotLight.LinearAttenuation = 0.253f; _spotLight.QuadraticAttenuation = 0.58f; - // Dispose previous CompositionLight if exists - CompositionLight?.Dispose(); - // Associate CompositionLight with XamlLight CompositionLight = _spotLight; @@ -52,10 +52,11 @@ protected override void OnConnected(UIElement targetElement) _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 @@ -123,10 +124,11 @@ public void Dispose() return; } - CompositionLight?.Dispose(); _lightPositionExpression?.Dispose(); _offsetAnimation?.Dispose(); + _hoverPosition?.Dispose(); _spotLight?.Dispose(); + CompositionLight?.Dispose(); _disposed = true; } } \ No newline at end of file diff --git a/AIDevGallery/Controls/OpacityMask.xaml.cs b/AIDevGallery/Controls/OpacityMask.xaml.cs index 213c312b..6d8748a9 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, IDisposable +public sealed partial class OpacityMaskView : ContentControl, IDisposable { // This is from Windows Community Toolkit Labs: https://github.com/CommunityToolkit/Labs-Windows/pull/491 @@ -30,10 +31,16 @@ public partial class OpacityMaskView : ContentControl, IDisposable private const string MaskContainerTemplateName = "PART_MaskContainer"; private const string RootGridTemplateName = "PART_RootGrid"; +#pragma warning disable IDISP002 // Compositor is provided by the system and should not be disposed private readonly Compositor _compositor = CompositionTarget.GetCompositorForCurrentThread(); +#pragma warning restore IDISP002 private CompositionBrush? _mask; private CompositionMaskBrush? _maskBrush; private SpriteVisual? _redirectVisual; + private CompositionVisualSurface? _contentVisualSurface; + private ExpressionAnimation? _contentSizeAnimation; + private CompositionVisualSurface? _maskVisualSurface; + private ExpressionAnimation? _maskSizeAnimation; /// /// Initializes a new instance of the class. @@ -64,9 +71,13 @@ protected override void OnApplyTemplate() _maskBrush?.Dispose(); _maskBrush = _compositor.CreateMaskBrush(); - _maskBrush.Source = GetVisualBrush(contentPresenter); + _contentVisualSurface?.Dispose(); + _contentSizeAnimation?.Dispose(); + _maskBrush.Source = GetVisualBrush(contentPresenter, ref _contentVisualSurface, ref _contentSizeAnimation); _mask?.Dispose(); - _mask = GetVisualBrush(maskContainer); + _maskVisualSurface?.Dispose(); + _maskSizeAnimation?.Dispose(); + _mask = GetVisualBrush(maskContainer, ref _maskVisualSurface, ref _maskSizeAnimation); _maskBrush.Mask = OpacityMask is null ? null : _mask; _redirectVisual?.Dispose(); @@ -76,17 +87,18 @@ protected override void OnApplyTemplate() 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; - using CompositionVisualSurface visualSurface = compositor.CreateVisualSurface(); + // Create visual surface and animation + visualSurface = compositor.CreateVisualSurface(); visualSurface.SourceVisual = visual; - using 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); @@ -107,8 +119,15 @@ private static void OnOpacityMaskChanged(DependencyObject d, DependencyPropertyC maskBrush.Mask = opacityMask is null ? null : self._mask; } + /// + /// Disposes the composition resources. + /// public void Dispose() { + _contentVisualSurface?.Dispose(); + _contentSizeAnimation?.Dispose(); + _maskVisualSurface?.Dispose(); + _maskSizeAnimation?.Dispose(); _mask?.Dispose(); _maskBrush?.Dispose(); _redirectVisual?.Dispose(); diff --git a/AIDevGallery/Controls/Shimmer.xaml.cs b/AIDevGallery/Controls/Shimmer.xaml.cs index 5bd6c248..08c7dea9 100644 --- a/AIDevGallery/Controls/Shimmer.xaml.cs +++ b/AIDevGallery/Controls/Shimmer.xaml.cs @@ -130,8 +130,10 @@ private bool TryInitializationResource() return false; } +#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(); @@ -150,10 +152,16 @@ private bool TryInitializationResource() SetGradientAndStops(); SetGradientStopColorsByTheme(); _rectangleGeometry.CornerRadius = new Vector2((float)CornerRadius.TopLeft); - _spriteShape?.Dispose(); + + // 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.Clear(); _shapeVisual.Shapes.Add(_spriteShape); ElementCompositionPreview.SetElementChildVisual(_shape, _shapeVisual); @@ -204,8 +212,11 @@ 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); @@ -281,6 +292,9 @@ public void Dispose() _gradientStop3?.Dispose(); _gradientStop4?.Dispose(); _spriteShape?.Dispose(); + _gradientStartPointAnimation?.Dispose(); + _gradientEndPointAnimation?.Dispose(); + _sizeAnimation?.Dispose(); _initialized = false; } From a6e25c23c0125af0877bb4bb348b1ec1e28da217 Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Mon, 5 Jan 2026 18:35:51 +0800 Subject: [PATCH 04/23] fix --- .../Controls/DownloadProgressList.xaml.cs | 16 ++++--- .../ModelPicker/AddHFModelView.xaml.cs | 18 ++++---- .../ModelPickerViews/OllamaPickerView.xaml.cs | 9 ++-- .../ModelPickerViews/OnnxPickerView.xaml.cs | 45 ++++++++++--------- .../ModelPickerViews/OpenAIPickerView.xaml.cs | 9 ++-- .../Controls/ModelSelectionControl.xaml.cs | 34 +++++++------- 6 files changed, 74 insertions(+), 57 deletions(-) diff --git a/AIDevGallery/Controls/DownloadProgressList.xaml.cs b/AIDevGallery/Controls/DownloadProgressList.xaml.cs index 10e07af9..bb35633a 100644 --- a/AIDevGallery/Controls/DownloadProgressList.xaml.cs +++ b/AIDevGallery/Controls/DownloadProgressList.xaml.cs @@ -72,9 +72,11 @@ private void RetryDownloadClicked(object sender, RoutedEventArgs e) if (sender is Button button && button.Tag is DownloadableModel downloadableModel) { downloadProgresses.Remove(downloadableModel); - using (App.ModelDownloadQueue.AddModel(downloadableModel.ModelDetails)) - { - } + + // 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 } } @@ -95,9 +97,11 @@ private void VerificationFailedClicked(object sender, RoutedEventArgs e) if (sender is Button button && button.Tag is DownloadableModel downloadableModel) { downloadProgresses.Remove(downloadableModel); - using (App.ModelDownloadQueue.AddModel(downloadableModel.ModelDetails)) - { - } + + // 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/ModelPicker/AddHFModelView.xaml.cs b/AIDevGallery/Controls/ModelPicker/AddHFModelView.xaml.cs index 951f0667..63cf1e0c 100644 --- a/AIDevGallery/Controls/ModelPicker/AddHFModelView.xaml.cs +++ b/AIDevGallery/Controls/ModelPicker/AddHFModelView.xaml.cs @@ -245,10 +245,11 @@ private async void DownloadModelClicked(object sender, RoutedEventArgs e) if (output == ContentDialogResult.Primary) { - using (App.ModelDownloadQueue.AddModel(result!.Details)) - { - result.State = ResultState.Downloading; - } + // 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; } } @@ -259,13 +260,14 @@ private void Hyperlink_Click(object sender, RoutedEventArgs e) string url = result!.License.LicenseUrl ?? $"https://huggingface.co/{result.SearchResult.Id}"; - using (Process.Start(new ProcessStartInfo() + // 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 } 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 924c0c3b..1ad00def 100644 --- a/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OllamaPickerView.xaml.cs +++ b/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OllamaPickerView.xaml.cs @@ -57,13 +57,14 @@ private void OllamaViewModelDetails_Click(object sender, RoutedEventArgs e) return; } - using (Process.Start(new ProcessStartInfo + // 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 = modelDetailsUrl, UseShellExecute = true - })) - { - } + }); +#pragma warning restore IDISP004 } } diff --git a/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OnnxPickerView.xaml.cs b/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OnnxPickerView.xaml.cs index 1e3a1f66..3920de52 100644 --- a/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OnnxPickerView.xaml.cs +++ b/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OnnxPickerView.xaml.cs @@ -227,9 +227,10 @@ private void OpenModelFolder_Click(object sender, RoutedEventArgs e) OpenModelFolderEvent.Log(cachedModel.Url); - using (Process.Start("explorer.exe", path!)) - { - } + // 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", path!); +#pragma warning restore IDISP004 } } } @@ -304,13 +305,14 @@ private void ViewLicense_Click(object sender, RoutedEventArgs e) licenseUrl = details.Url; } - using (Process.Start(new ProcessStartInfo() + // 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 = licenseUrl, UseShellExecute = true - })) - { - } + }); +#pragma warning restore IDISP004 } } @@ -387,24 +389,26 @@ private void OpenAIToolkitButton_Click(object sender, RoutedEventArgs e) bool wasDeeplinkSuccessful = true; try { - using (Process.Start(new ProcessStartInfo() + // Process.Start for external process doesn't need disposal - process lifecycle is independent +#pragma warning disable IDISP004 // Don't ignore created IDisposable + Process.Start(new ProcessStartInfo() { FileName = toolkitDeeplink, UseShellExecute = true - })) - { - } + }); +#pragma warning restore IDISP004 } catch { - using (Process.Start(new ProcessStartInfo() + // 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 = "https://learn.microsoft.com/en-us/windows/ai/toolkit/", UseShellExecute = true - })) - { - wasDeeplinkSuccessful = false; - } + }); +#pragma warning restore IDISP004 + wasDeeplinkSuccessful = false; } finally { @@ -414,13 +418,14 @@ private void OpenAIToolkitButton_Click(object sender, RoutedEventArgs e) private void ViewDocumentationButton_Click(object sender, RoutedEventArgs e) { - using (Process.Start(new ProcessStartInfo() + // 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 = "https://aka.ms/winml-gallery-tutorial", UseShellExecute = true - })) - { - } + }); +#pragma warning restore IDISP004 } 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 385c8d18..de579fb4 100644 --- a/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OpenAIPickerView.xaml.cs +++ b/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OpenAIPickerView.xaml.cs @@ -71,13 +71,14 @@ private void ViewModelDetails_Click(object sender, RoutedEventArgs e) return; } - using (Process.Start(new ProcessStartInfo + // 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 = modelDetailsUrl, UseShellExecute = true - })) - { - } + }); +#pragma warning restore IDISP004 } } diff --git a/AIDevGallery/Controls/ModelSelectionControl.xaml.cs b/AIDevGallery/Controls/ModelSelectionControl.xaml.cs index 7730ce4b..df492599 100644 --- a/AIDevGallery/Controls/ModelSelectionControl.xaml.cs +++ b/AIDevGallery/Controls/ModelSelectionControl.xaml.cs @@ -323,9 +323,10 @@ private void OpenModelFolder_Click(object sender, RoutedEventArgs e) OpenModelFolderEvent.Log(cachedModel.Url); - using (Process.Start("explorer.exe", path!)) - { - } + // 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", path!); +#pragma warning restore IDISP004 } } } @@ -383,13 +384,14 @@ private void ModelCard_Click(object sender, RoutedEventArgs e) } } - using (Process.Start(new ProcessStartInfo() + // 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 = modelcardUrl, UseShellExecute = true - })) - { - } + }); +#pragma warning restore IDISP004 } } } @@ -435,13 +437,14 @@ private void ViewLicense_Click(object sender, RoutedEventArgs e) licenseUrl = details.Url; } - using (Process.Start(new ProcessStartInfo() + // 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 = licenseUrl, UseShellExecute = true - })) - { - } + }); +#pragma warning restore IDISP004 } } @@ -611,13 +614,14 @@ private void ViewModelDetails_Click(object sender, RoutedEventArgs e) return; } - using (Process.Start(new ProcessStartInfo + // 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 = modelDetailsUrl, UseShellExecute = true - })) - { - } + }); +#pragma warning restore IDISP004 } } } \ No newline at end of file From 836ffda134825e117889b9be8f25e0c9aad6f623 Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Mon, 5 Jan 2026 18:45:00 +0800 Subject: [PATCH 05/23] fix --- AIDevGallery/Pages/APIs/APIPage.xaml.cs | 10 +++++--- AIDevGallery/Pages/Models/ModelPage.xaml.cs | 28 ++++++++++++--------- AIDevGallery/Pages/SettingsPage.xaml.cs | 14 ++++++----- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/AIDevGallery/Pages/APIs/APIPage.xaml.cs b/AIDevGallery/Pages/APIs/APIPage.xaml.cs index ef114e35..e32569b5 100644 --- a/AIDevGallery/Pages/APIs/APIPage.xaml.cs +++ b/AIDevGallery/Pages/APIs/APIPage.xaml.cs @@ -184,13 +184,15 @@ private void MarkdownTextBlock_OnLinkClicked(object sender, CommunityToolkit.Lab } ModelDetailsLinkClickedEvent.Log(link); - using (Process.Start(new ProcessStartInfo() + + // 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 = link, UseShellExecute = true - })) - { - } + }); +#pragma warning restore IDISP004 } 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 d1ec1137..c1446658 100644 --- a/AIDevGallery/Pages/Models/ModelPage.xaml.cs +++ b/AIDevGallery/Pages/Models/ModelPage.xaml.cs @@ -252,24 +252,26 @@ private void ToolkitActionFlyoutItem_Click(object sender, RoutedEventArgs e) bool wasDeeplinkSuccesful = true; try { - using (Process.Start(new ProcessStartInfo() + // Process.Start for external process doesn't need disposal - process lifecycle is independent +#pragma warning disable IDISP004 // Don't ignore created IDisposable + Process.Start(new ProcessStartInfo() { FileName = toolkitDeeplink, UseShellExecute = true - })) - { - } + }); +#pragma warning restore IDISP004 } catch { - using (Process.Start(new ProcessStartInfo() + // Process.Start for external process doesn't need disposal - process lifecycle is independent +#pragma warning disable IDISP004 // Don't ignore created IDisposable + Process.Start(new ProcessStartInfo() { FileName = "https://learn.microsoft.com/en-us/windows/ai/toolkit/", UseShellExecute = true - })) - { - wasDeeplinkSuccesful = false; - } + }); +#pragma warning restore IDISP004 + wasDeeplinkSuccesful = false; } finally { @@ -323,9 +325,11 @@ private void MarkdownTextBlock_OnLinkClicked(object sender, CommunityToolkit.Lab FileName = uri.AbsoluteUri, UseShellExecute = true }; - using (Process.Start(psi)) - { - } + + // 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(psi); +#pragma warning restore IDISP004 } 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 894e4b7d..bb2cf151 100644 --- a/AIDevGallery/Pages/SettingsPage.xaml.cs +++ b/AIDevGallery/Pages/SettingsPage.xaml.cs @@ -95,9 +95,10 @@ private void FolderPathTxt_Click(object sender, RoutedEventArgs e) { if (cacheFolderPath != null) { - using (Process.Start("explorer.exe", cacheFolderPath)) - { - } + // 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", cacheFolderPath); +#pragma warning restore IDISP004 } } @@ -203,9 +204,10 @@ private void ModelFolder_Click(object sender, RoutedEventArgs e) if (path != null && Directory.Exists(path)) { - using (Process.Start("explorer.exe", path)) - { - } + // 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", path); +#pragma warning restore IDISP004 } } } From 07bf8eb9526beb4ef24b5b2cd4726b60fedc173c Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Mon, 5 Jan 2026 18:57:01 +0800 Subject: [PATCH 06/23] fix --- .../Language Models/SemanticKernelChat.xaml.cs | 9 +++++---- .../Open Source Models/Language Models/Summarize.xaml.cs | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml.cs b/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml.cs index 45335fbe..80dc6e0e 100644 --- a/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml.cs @@ -63,7 +63,11 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa IChatClient? model = null; try { + // The model's lifetime is transferred to the Semantic Kernel framework after AsChatCompletionService(). + // The framework manages the disposal, so we suppress IDISP001 for this created instance. +#pragma warning disable IDISP001 // Dispose created model = await sampleParams.GetIChatClientAsync(); +#pragma warning restore IDISP001 // Dispose created } catch (Exception ex) { @@ -75,12 +79,9 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa return; } - using (model) - { #pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - _chatCompletionService = model.AsChatCompletionService(); + _chatCompletionService = model.AsChatCompletionService(); #pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - } IKernelBuilder builder = Kernel.CreateBuilder(); _semanticKernel = builder.Build(); diff --git a/AIDevGallery/Samples/Open Source Models/Language Models/Summarize.xaml.cs b/AIDevGallery/Samples/Open Source Models/Language Models/Summarize.xaml.cs index 05bde7d2..9c2229a3 100644 --- a/AIDevGallery/Samples/Open Source Models/Language Models/Summarize.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Language Models/Summarize.xaml.cs @@ -40,7 +40,7 @@ protected override async Task LoadModelAsync(SampleNavigationParameters samplePa { try { - (chatClient as IDisposable)?.Dispose(); + chatClient?.Dispose(); chatClient = await sampleParams.GetIChatClientAsync(); InputTextBox.MaxLength = _defaultMaxLength; } From 1597cf4f70441592cc305cae2decbdecd93773c7 Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Mon, 5 Jan 2026 19:06:08 +0800 Subject: [PATCH 07/23] fix --- .../Samples/SharedCode/BitmapFunctions.cs | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/AIDevGallery/Samples/SharedCode/BitmapFunctions.cs b/AIDevGallery/Samples/SharedCode/BitmapFunctions.cs index 7202620f..e63b5272 100644 --- a/AIDevGallery/Samples/SharedCode/BitmapFunctions.cs +++ b/AIDevGallery/Samples/SharedCode/BitmapFunctions.cs @@ -96,20 +96,19 @@ public static async Task ResizeVideoFrameWithPadding(VideoFrame videoFra stream.Seek(0); // Reset stream position // Convert to System.Drawing.Bitmap - Bitmap paddedBitmap; - using (var streamWrapper = stream.AsStream()) - using (var tempBitmap = new Bitmap(streamWrapper)) - { - paddedBitmap = new(targetWidth, targetHeight); + // IDISP: AsStream() returns a wrapper view of the same stream, disposing it here would close the underlying stream prematurely +#pragma warning disable IDISP004 // Don't ignore created IDisposable + using var tempBitmap = new Bitmap(stream.AsStream()); +#pragma warning restore IDISP004 + Bitmap paddedBitmap = new(targetWidth, targetHeight); - using (Graphics graphics = Graphics.FromImage(paddedBitmap)) - { - graphics.Clear(Color.White); // White padding background - graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; + using (Graphics graphics = Graphics.FromImage(paddedBitmap)) + { + graphics.Clear(Color.White); // White padding background + graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; - // Draw the resized image centered - graphics.DrawImage(tempBitmap, offsetX, offsetY, scaledWidth, scaledHeight); - } + // Draw the resized image centered + graphics.DrawImage(tempBitmap, offsetX, offsetY, scaledWidth, scaledHeight); } return paddedBitmap; @@ -321,8 +320,11 @@ public static BitmapImage RenderPredictions(Bitmap image, List predi memoryStream.Position = 0; - using var randomAccessStream = memoryStream.AsRandomAccessStream(); - bitmapImage.SetSource(randomAccessStream); + // IDISP: AsRandomAccessStream() returns a wrapper of the MemoryStream. The MemoryStream is already in a using block and will be disposed properly. + // Disposing the wrapper here would close the underlying MemoryStream prematurely before SetSource can use it. +#pragma warning disable IDISP004 // Don't ignore created IDisposable + bitmapImage.SetSource(memoryStream.AsRandomAccessStream()); +#pragma warning restore IDISP004 } return bitmapImage; @@ -356,8 +358,12 @@ public static BitmapImage RenderPredictions(Bitmap image, List predi { image.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png); memoryStream.Position = 0; - using var randomAccessStream = memoryStream.AsRandomAccessStream(); - bitmapImage.SetSource(randomAccessStream); + + // IDISP: AsRandomAccessStream() returns a wrapper of the MemoryStream. The MemoryStream is already in a using block and will be disposed properly. + // Disposing the wrapper here would close the underlying MemoryStream prematurely before SetSource can use it. +#pragma warning disable IDISP004 // Don't ignore created IDisposable + bitmapImage.SetSource(memoryStream.AsRandomAccessStream()); +#pragma warning restore IDISP004 } return bitmapImage; @@ -475,11 +481,11 @@ public static BitmapImage ConvertBitmapToBitmapImage(Bitmap bitmap) using var stream = new InMemoryRandomAccessStream(); // Save the bitmap to a stream - using (var streamWrapper = stream.AsStream()) - { - bitmap.Save(streamWrapper, ImageFormat.Png); - } - + // IDISP: AsStream() returns a wrapper view of the InMemoryRandomAccessStream. The stream is already in a using statement and will be disposed properly. + // Disposing the wrapper here would close the underlying stream prematurely. +#pragma warning disable IDISP004 // Don't ignore created IDisposable + bitmap.Save(stream.AsStream(), ImageFormat.Png); +#pragma warning restore IDISP004 stream.Seek(0); // Create a BitmapImage from the stream From d76bc0c596de8fd66aff5557a1b5a76ac5203c86 Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Mon, 5 Jan 2026 19:15:16 +0800 Subject: [PATCH 08/23] fix --- .../Samples/WCRAPIs/BackgroundRemover.xaml.cs | 1 + .../Samples/WCRAPIs/ForegroundExtractor.xaml.cs | 9 +++++++++ AIDevGallery/Samples/WCRAPIs/MagicEraser.xaml.cs | 14 +++++++++++++- AIDevGallery/Samples/WCRAPIs/RestyleImage.xaml.cs | 7 +++++-- AIDevGallery/Samples/WCRAPIs/SDXL.xaml.cs | 7 +++++-- 5 files changed, 33 insertions(+), 5 deletions(-) diff --git a/AIDevGallery/Samples/WCRAPIs/BackgroundRemover.xaml.cs b/AIDevGallery/Samples/WCRAPIs/BackgroundRemover.xaml.cs index 3d11e83c..5c5b23d6 100644 --- a/AIDevGallery/Samples/WCRAPIs/BackgroundRemover.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/BackgroundRemover.xaml.cs @@ -258,6 +258,7 @@ private async void RemoveBackground_Click(object sender, RoutedEventArgs e) return; } + // outputBitmap is disposed after display, as SetImageSource creates a copy using var outputBitmap = await ExtractBackground(_inputBitmap, _selectionPoints); if (outputBitmap != null) { diff --git a/AIDevGallery/Samples/WCRAPIs/ForegroundExtractor.xaml.cs b/AIDevGallery/Samples/WCRAPIs/ForegroundExtractor.xaml.cs index 99b2153c..16f35cd3 100644 --- a/AIDevGallery/Samples/WCRAPIs/ForegroundExtractor.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/ForegroundExtractor.xaml.cs @@ -181,6 +181,15 @@ private async Task SetImage(Image image, SoftwareBitmap? bitmap) return; } + // Dispose previous source to avoid memory leaks + // Image.Source is managed by this class, not injected, so disposal is appropriate +#pragma warning disable IDISP007 // Don't dispose injected - Image.Source is managed by this class + if (image.Source is SoftwareBitmapSource previousSource) + { + previousSource.Dispose(); + } +#pragma warning restore IDISP007 + var convertedBitmap = SoftwareBitmap.Convert(bitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); var bitmapSource = new SoftwareBitmapSource(); diff --git a/AIDevGallery/Samples/WCRAPIs/MagicEraser.xaml.cs b/AIDevGallery/Samples/WCRAPIs/MagicEraser.xaml.cs index 5542811f..d563d9e6 100644 --- a/AIDevGallery/Samples/WCRAPIs/MagicEraser.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/MagicEraser.xaml.cs @@ -33,7 +33,7 @@ namespace AIDevGallery.Samples.WCRAPIs; "WinDev.png" ], Icon = "\uEE6F")] -internal sealed partial class MagicEraser : BaseSamplePage +internal sealed partial class MagicEraser : BaseSamplePage, IDisposable { private SoftwareBitmap? _inputBitmap; private SoftwareBitmap? _maskBitmap; @@ -46,6 +46,18 @@ public MagicEraser() this.InitializeComponent(); } + public void Dispose() + { + while (_bitmaps.Count > 0) + { + _bitmaps.Pop()?.Dispose(); + } + + _inputBitmap?.Dispose(); + _maskBitmap?.Dispose(); + _eraser?.Dispose(); + } + protected override async Task LoadModelAsync(SampleNavigationParameters sampleParams) { var readyState = ImageObjectRemover.GetReadyState(); diff --git a/AIDevGallery/Samples/WCRAPIs/RestyleImage.xaml.cs b/AIDevGallery/Samples/WCRAPIs/RestyleImage.xaml.cs index 04816961..210ad833 100644 --- a/AIDevGallery/Samples/WCRAPIs/RestyleImage.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/RestyleImage.xaml.cs @@ -177,8 +177,11 @@ public async Task GenerateImage(string prompt) return; } - using var softwareBitmap = result.Image.CopyToSoftwareBitmap(); - using var convertedImage = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); + // Bitmap ownership is transferred to SoftwareBitmapSource, suppress IDISP001 warning +#pragma warning disable IDISP001 // Dispose created - ownership transferred to SoftwareBitmapSource + var softwareBitmap = result.Image.CopyToSoftwareBitmap(); + var convertedImage = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); +#pragma warning restore IDISP001 if (convertedImage != null) { var source = new SoftwareBitmapSource(); diff --git a/AIDevGallery/Samples/WCRAPIs/SDXL.xaml.cs b/AIDevGallery/Samples/WCRAPIs/SDXL.xaml.cs index 2e1ded0d..351c49b1 100644 --- a/AIDevGallery/Samples/WCRAPIs/SDXL.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/SDXL.xaml.cs @@ -161,8 +161,11 @@ public async Task GenerateImage(string prompt) return; } - using var softwareBitmap = result.Image.CopyToSoftwareBitmap(); - using var convertedImage = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); + // Bitmap ownership is transferred to SoftwareBitmapSource, suppress IDISP001 warning +#pragma warning disable IDISP001 // Dispose created - ownership transferred to SoftwareBitmapSource + var softwareBitmap = result.Image.CopyToSoftwareBitmap(); + var convertedImage = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); +#pragma warning restore IDISP001 if (convertedImage != null) { var source = new SoftwareBitmapSource(); From 8bdaf0aa2b1978637dcbf21d13516ae6ba45c84e Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Mon, 5 Jan 2026 19:22:54 +0800 Subject: [PATCH 09/23] fix --- AIDevGallery/Utils/HuggingFaceApi.cs | 6 +++--- AIDevGallery/Utils/ModelDownloadQueue.cs | 1 + AIDevGallery/ViewModels/DownloadableModel.cs | 4 +++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/AIDevGallery/Utils/HuggingFaceApi.cs b/AIDevGallery/Utils/HuggingFaceApi.cs index 43296248..8c9a9c8e 100644 --- a/AIDevGallery/Utils/HuggingFaceApi.cs +++ b/AIDevGallery/Utils/HuggingFaceApi.cs @@ -28,7 +28,7 @@ internal class HuggingFaceApi public static async Task?> FindModels(string query, string filter = "onnx") { string searchUrl = $"{HFApiUrl}/models?search={query}&filter={filter}&full=true&config=true"; - using var response = await _httpClient.GetAsync(searchUrl); + var response = await _httpClient.GetAsync(searchUrl); var responseContent = await response.Content.ReadAsStringAsync(); try @@ -53,7 +53,7 @@ public static async Task GetContentsOfTextFile(string modelId, string fi { var url = $"{HFUrl}/{modelId}/resolve/{commitOrBranch}/{filePath}"; - using var response = await _httpClient.GetAsync(url); + var response = await _httpClient.GetAsync(url); return await response.Content.ReadAsStringAsync(); } @@ -68,7 +68,7 @@ public static async Task GetContentsOfTextFile(string fileUrl) var requestUrl = $"{HFUrl}/{url.Organization}/{url.Repo}/resolve/{url.Ref}/{url.Path}"; - using var response = await _httpClient.GetAsync(requestUrl); + var response = await _httpClient.GetAsync(requestUrl); return await response.Content.ReadAsStringAsync(); } } \ No newline at end of file diff --git a/AIDevGallery/Utils/ModelDownloadQueue.cs b/AIDevGallery/Utils/ModelDownloadQueue.cs index 94e254f6..d07430e8 100644 --- a/AIDevGallery/Utils/ModelDownloadQueue.cs +++ b/AIDevGallery/Utils/ModelDownloadQueue.cs @@ -88,6 +88,7 @@ public void CancelModelDownload(ModelDownload download) ModelDownloadCancelEvent.Log(download.Details.Url); _queue.Remove(download); ModelsChanged?.Invoke(this); + download.Dispose(); } public IReadOnlyList GetDownloads() diff --git a/AIDevGallery/ViewModels/DownloadableModel.cs b/AIDevGallery/ViewModels/DownloadableModel.cs index 2364cca6..062cc144 100644 --- a/AIDevGallery/ViewModels/DownloadableModel.cs +++ b/AIDevGallery/ViewModels/DownloadableModel.cs @@ -27,7 +27,9 @@ internal partial class DownloadableModel : BaseModel public bool IsDownloadEnabled => Compatibility.CompatibilityState != ModelCompatibilityState.NotCompatible; -#pragma warning disable IDISP008 // Don't assign member with injected and created disposables - ModelDownload lifecycle is managed by ModelDownloadQueue +#pragma warning disable IDISP008 // Don't assign member with injected and created disposables + // ModelDownload lifecycle is managed by ModelDownloadQueue (disposed in ProcessDownloads and CancelModelDownload methods) + // This class only holds a reference and should not dispose it private ModelDownload? _modelDownload; public ModelDownload? ModelDownload From 576f3d2483ed715bace81dfb6868e9bb09da540c Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Tue, 6 Jan 2026 11:28:20 +0800 Subject: [PATCH 10/23] Add UT --- .../Controls/DefaultSVGRendererTests.cs | 197 ++++++++++ .../Samples/ResourceLifecycleTests.cs | 363 ++++++++++++++++++ 2 files changed, 560 insertions(+) create mode 100644 AIDevGallery.Tests/UnitTests/Controls/DefaultSVGRendererTests.cs create mode 100644 AIDevGallery.Tests/UnitTests/Samples/ResourceLifecycleTests.cs diff --git a/AIDevGallery.Tests/UnitTests/Controls/DefaultSVGRendererTests.cs b/AIDevGallery.Tests/UnitTests/Controls/DefaultSVGRendererTests.cs new file mode 100644 index 00000000..ab22fa51 --- /dev/null +++ b/AIDevGallery.Tests/UnitTests/Controls/DefaultSVGRendererTests.cs @@ -0,0 +1,197 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media.Imaging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Threading.Tasks; + +namespace AIDevGallery.Tests.UnitTests.Controls; + +[TestClass] +public class DefaultSVGRendererTests +{ + private DefaultSVGRenderer? _renderer; + + [TestInitialize] + public void TestInitialize() + { + _renderer = new DefaultSVGRenderer(); + } + + [TestMethod] + public async Task SvgToImage_WithValidSvgString_ReturnsImageWithSource() + { + // Arrange + var svgString = @" + + + "; + + // Act + var image = await _renderer!.SvgToImage(svgString); + + // Assert + Assert.IsNotNull(image, "Image should not be null"); + Assert.IsNotNull(image.Source, "Image.Source should not be null"); + Assert.IsInstanceOfType(image.Source, typeof(SvgImageSource), "Image.Source should be SvgImageSource"); + } + + [TestMethod] + public async Task SvgToImage_WithValidSvgString_SetsCorrectDimensions() + { + // Arrange + var svgString = @" + + + "; + + // Act + var image = await _renderer!.SvgToImage(svgString); + + // Assert + Assert.AreEqual(200.0, image.Width, "Image width should match SVG width"); + Assert.AreEqual(150.0, image.Height, "Image height should match SVG height"); + } + + [TestMethod] + public async Task SvgToImage_WithSvgWithoutDimensions_ReturnsImageWithoutDimensions() + { + // Arrange + var svgString = @" + + + "; + + // Act + var image = await _renderer!.SvgToImage(svgString); + + // Assert + Assert.IsNotNull(image); + Assert.IsNotNull(image.Source); + // Width and Height should be 0 or default if not specified in SVG + } + + [TestMethod] + public async Task SvgToImage_StreamIsClosedAfterLoad_ImageSourceRemainsValid() + { + // Arrange + var svgString = @" + + + "; + + // Act + var image = await _renderer!.SvgToImage(svgString); + + // This test verifies that the using statement for randomAccessStream + // doesn't cause issues. The stream is closed after SetSourceAsync, + // but the image source should still be valid because SetSourceAsync + // copies the data synchronously. + + // Assert + Assert.IsNotNull(image.Source, "Image source should remain valid after stream is disposed"); + + // Additional verification: try to access properties + var svgSource = image.Source as SvgImageSource; + Assert.IsNotNull(svgSource, "Should be able to cast to SvgImageSource"); + } + + [TestMethod] + public async Task SvgToImage_ComplexSvg_LoadsSuccessfully() + { + // Arrange + var complexSvg = @" + + + + + + + + + SVG Test + "; + + // Act + var image = await _renderer!.SvgToImage(complexSvg); + + // Assert + Assert.IsNotNull(image); + Assert.IsNotNull(image.Source); + Assert.AreEqual(300.0, image.Width); + Assert.AreEqual(200.0, image.Height); + } + + [TestMethod] + public async Task SvgToImage_MultipleCallsInSequence_AllSucceed() + { + // This test verifies that multiple sequential calls work correctly + // and that resource disposal doesn't interfere with subsequent calls + + // Arrange + var svgStrings = new[] + { + @"", + @"", + @"" + }; + + // Act & Assert + foreach (var svgString in svgStrings) + { + var image = await _renderer!.SvgToImage(svgString); + Assert.IsNotNull(image); + Assert.IsNotNull(image.Source); + } + } + + [TestMethod] + [ExpectedException(typeof(Exception))] + public async Task SvgToImage_WithInvalidSvg_ThrowsException() + { + // Arrange + var invalidSvg = "This is not valid SVG"; + + // Act + // This should throw an exception during SetSourceAsync + await _renderer!.SvgToImage(invalidSvg); + + // Assert is handled by ExpectedException + } + + [TestMethod] + public async Task SvgToImage_WithEmptySvg_HandlesGracefully() + { + // Arrange + var emptySvg = @""; + + // Act + var image = await _renderer!.SvgToImage(emptySvg); + + // Assert + Assert.IsNotNull(image); + Assert.IsNotNull(image.Source); + } + + [TestMethod] + public async Task SvgToImage_WithUnicodeCharacters_LoadsSuccessfully() + { + // Arrange + var svgWithUnicode = @" + + 你好世界 🌍 + "; + + // Act + var image = await _renderer!.SvgToImage(svgWithUnicode); + + // Assert + Assert.IsNotNull(image); + Assert.IsNotNull(image.Source); + Assert.AreEqual(200.0, image.Width); + Assert.AreEqual(100.0, image.Height); + } +} diff --git a/AIDevGallery.Tests/UnitTests/Samples/ResourceLifecycleTests.cs b/AIDevGallery.Tests/UnitTests/Samples/ResourceLifecycleTests.cs new file mode 100644 index 00000000..6481bbb1 --- /dev/null +++ b/AIDevGallery.Tests/UnitTests/Samples/ResourceLifecycleTests.cs @@ -0,0 +1,363 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AIDevGallery.Samples.SharedCode; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Net.Http; +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 SoftwareBitmap_Convert_CreatesNewCopy() + { + // Arrange + var originalBitmap = new SoftwareBitmap( + BitmapPixelFormat.Rgba8, + 100, + 100, + BitmapAlphaMode.Premultiplied); + + // Act + var convertedBitmap = SoftwareBitmap.Convert( + originalBitmap, + BitmapPixelFormat.Bgra8, + BitmapAlphaMode.Premultiplied); + + // Assert + 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"); + + // Clean up + originalBitmap.Dispose(); + convertedBitmap.Dispose(); + } + + [TestMethod] + public void SoftwareBitmap_CanDisposeOriginalAfterConvert() + { + // This test verifies that disposing the original bitmap after Convert + // doesn't affect the converted bitmap (they are independent copies) + + // Arrange + var originalBitmap = new SoftwareBitmap( + BitmapPixelFormat.Rgba8, + 50, + 50, + BitmapAlphaMode.Premultiplied); + + // Act + var convertedBitmap = SoftwareBitmap.Convert( + originalBitmap, + BitmapPixelFormat.Bgra8, + BitmapAlphaMode.Premultiplied); + + // Dispose original + originalBitmap.Dispose(); + + // Assert - converted bitmap should still be usable + Assert.AreEqual(50, convertedBitmap.PixelWidth); + Assert.AreEqual(50, convertedBitmap.PixelHeight); + Assert.AreEqual(BitmapPixelFormat.Bgra8, convertedBitmap.BitmapPixelFormat); + + // Clean up + convertedBitmap.Dispose(); + } + + [TestMethod] + [ExpectedException(typeof(ObjectDisposedException))] + public void SoftwareBitmap_AccessAfterDispose_ThrowsException() + { + // Arrange + var bitmap = new SoftwareBitmap( + BitmapPixelFormat.Bgra8, + 100, + 100, + BitmapAlphaMode.Premultiplied); + + // Act + bitmap.Dispose(); + + // Assert - accessing properties after dispose should throw + _ = bitmap.PixelWidth; // This should throw ObjectDisposedException + } + + [TestMethod] + public void SoftwareBitmap_UsingPattern_DisposesCorrectly() + { + // This test verifies the using pattern works correctly + SoftwareBitmap? bitmapReference = null; + + // Act + using (var bitmap = new SoftwareBitmap( + BitmapPixelFormat.Bgra8, + 100, + 100, + BitmapAlphaMode.Premultiplied)) + { + bitmapReference = bitmap; + Assert.AreEqual(100, bitmap.PixelWidth); + } + + // Assert - bitmap should be disposed after using block + try + { + _ = bitmapReference!.PixelWidth; + Assert.Fail("Should have thrown ObjectDisposedException"); + } + catch (ObjectDisposedException) + { + // Expected + } + } + + [TestMethod] + public async Task SimulateBackgroundRemoverPattern_VerifyLifecycle() + { + // This test simulates the pattern used in BackgroundRemover.xaml.cs + // to verify that disposing outputBitmap after SetImageSource is safe + + // 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 + // This is what happens inside SetImageSource: + convertedBitmap = SoftwareBitmap.Convert( + outputBitmap, + BitmapPixelFormat.Bgra8, + BitmapAlphaMode.Premultiplied); + + // At this point in BackgroundRemover, we would: + // await bitmapSource.SetBitmapAsync(convertedBitmap); + // For testing, we just verify the converted bitmap is valid + + Assert.IsNotNull(convertedBitmap); + Assert.AreEqual(200, convertedBitmap.PixelWidth); + + // Now simulate the using block exiting - dispose outputBitmap + outputBitmap.Dispose(); + + // Assert - convertedBitmap should still be valid because it's a copy + Assert.AreEqual(200, convertedBitmap.PixelWidth); + Assert.AreEqual(200, convertedBitmap.PixelHeight); + + await Task.CompletedTask; // Simulate async operation + } + finally + { + // Clean up + convertedBitmap?.Dispose(); + } + } + + [TestMethod] + public void MultipleConversions_EachCreateIndependentCopies() + { + // Arrange + var originalBitmap = new SoftwareBitmap( + BitmapPixelFormat.Rgba8, + 100, + 100, + BitmapAlphaMode.Premultiplied); + + // Act - Convert multiple times + var converted1 = SoftwareBitmap.Convert(originalBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); + var converted2 = SoftwareBitmap.Convert(originalBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); + var converted3 = SoftwareBitmap.Convert(originalBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); + + // Assert - All should be different instances + Assert.AreNotSame(converted1, converted2); + Assert.AreNotSame(converted2, converted3); + Assert.AreNotSame(converted1, converted3); + + // Dispose one shouldn't affect others + converted1.Dispose(); + Assert.AreEqual(100, converted2.PixelWidth); // Should still work + Assert.AreEqual(100, converted3.PixelWidth); // Should still work + + // Clean up + originalBitmap.Dispose(); + converted2.Dispose(); + converted3.Dispose(); + } +} + +/// +/// Performance tests for HttpClient usage patterns +/// Note: These are integration tests that make actual HTTP calls +/// Mark with [Ignore] if you want to skip them in regular test runs +/// +[TestClass] +public class HttpClientPerformanceTests +{ + private const string TestImageUrl = "https://via.placeholder.com/150"; + private const int WarmupIterations = 2; + private const int TestIterations = 10; + + [TestMethod] + [TestCategory("Performance")] + public async Task HttpClient_NewInstanceEachTime_MeasurePerformance() + { + // Warmup + for (int i = 0; i < WarmupIterations; i++) + { + using var client = new HttpClient(); + using var response = await client.GetAsync(TestImageUrl); + } + + // Measure + var stopwatch = Stopwatch.StartNew(); + for (int i = 0; i < TestIterations; i++) + { + using var client = new HttpClient(); + using var response = await client.GetAsync(TestImageUrl); + _ = await response.Content.ReadAsByteArrayAsync(); + } + stopwatch.Stop(); + + var avgTime = stopwatch.ElapsedMilliseconds / (double)TestIterations; + Debug.WriteLine($"New HttpClient each time - Average: {avgTime:F2}ms, Total: {stopwatch.ElapsedMilliseconds}ms"); + + // No assertion, just measuring + Assert.IsTrue(stopwatch.ElapsedMilliseconds > 0); + } + + [TestMethod] + [TestCategory("Performance")] + public async Task HttpClient_SharedInstance_MeasurePerformance() + { + var sharedClient = new HttpClient(); + + // Warmup + for (int i = 0; i < WarmupIterations; i++) + { + using var response = await sharedClient.GetAsync(TestImageUrl); + } + + // Measure + var stopwatch = Stopwatch.StartNew(); + for (int i = 0; i < TestIterations; i++) + { + using var response = await sharedClient.GetAsync(TestImageUrl); + _ = await response.Content.ReadAsByteArrayAsync(); + } + stopwatch.Stop(); + + var avgTime = stopwatch.ElapsedMilliseconds / (double)TestIterations; + Debug.WriteLine($"Shared HttpClient - Average: {avgTime:F2}ms, Total: {stopwatch.ElapsedMilliseconds}ms"); + + // No assertion, just measuring + Assert.IsTrue(stopwatch.ElapsedMilliseconds > 0); + + sharedClient.Dispose(); + } + + [TestMethod] + [TestCategory("Performance")] + public async Task HttpClient_ComparePerformance() + { + const int iterations = 5; + var results = new Dictionary(); + + // Test 1: New instance each time + var sw1 = Stopwatch.StartNew(); + for (int i = 0; i < iterations; i++) + { + using var client = new HttpClient(); + using var response = await client.GetAsync(TestImageUrl); + _ = await response.Content.ReadAsByteArrayAsync(); + } + sw1.Stop(); + results["NewInstance"] = sw1.ElapsedMilliseconds; + + // Test 2: Shared instance + var sharedClient = new HttpClient(); + var sw2 = Stopwatch.StartNew(); + for (int i = 0; i < iterations; i++) + { + using var response = await sharedClient.GetAsync(TestImageUrl); + _ = await response.Content.ReadAsByteArrayAsync(); + } + sw2.Stop(); + results["SharedInstance"] = sw2.ElapsedMilliseconds; + sharedClient.Dispose(); + + // Report results + var improvement = ((results["NewInstance"] - results["SharedInstance"]) / (double)results["NewInstance"]) * 100; + Debug.WriteLine($"\n=== HttpClient Performance Comparison ==="); + Debug.WriteLine($"New instance each time: {results["NewInstance"]}ms"); + Debug.WriteLine($"Shared instance: {results["SharedInstance"]}ms"); + Debug.WriteLine($"Performance improvement: {improvement:F1}%"); + Debug.WriteLine($"=========================================\n"); + + // Assert that shared instance is at least not worse + // (In reality, it should be significantly better) + Assert.IsTrue(results["SharedInstance"] <= results["NewInstance"] * 1.2, + $"Shared HttpClient should not be significantly slower. New: {results["NewInstance"]}ms, Shared: {results["SharedInstance"]}ms"); + } + + [TestMethod] + [TestCategory("Performance")] + [Ignore] // Ignore by default as it takes time + public async Task HttpClient_SocketExhaustion_Simulate() + { + // This test simulates the socket exhaustion problem + // by creating many HttpClient instances rapidly + + var tasks = new List(); + var errorCount = 0; + + try + { + for (int i = 0; i < 100; i++) + { + tasks.Add(Task.Run(async () => + { + try + { + using var client = new HttpClient(); + using var response = await client.GetAsync(TestImageUrl); + } + catch (HttpRequestException) + { + System.Threading.Interlocked.Increment(ref errorCount); + } + })); + } + + await Task.WhenAll(tasks); + + Debug.WriteLine($"Errors encountered: {errorCount}/100"); + + // We expect some errors due to socket exhaustion + // This demonstrates the problem with creating new HttpClient instances + } + catch (Exception ex) + { + Debug.WriteLine($"Exception: {ex.Message}"); + } + } +} From 5d6bbeaa675a8a69bb80425aef1a5ebeacd3054a Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Tue, 6 Jan 2026 11:48:14 +0800 Subject: [PATCH 11/23] Fix --- AIDevGallery/Utils/HuggingFaceApi.cs | 6 +++--- AIDevGallery/Utils/ModelDownloadQueue.cs | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/AIDevGallery/Utils/HuggingFaceApi.cs b/AIDevGallery/Utils/HuggingFaceApi.cs index 8c9a9c8e..43296248 100644 --- a/AIDevGallery/Utils/HuggingFaceApi.cs +++ b/AIDevGallery/Utils/HuggingFaceApi.cs @@ -28,7 +28,7 @@ internal class HuggingFaceApi public static async Task?> FindModels(string query, string filter = "onnx") { string searchUrl = $"{HFApiUrl}/models?search={query}&filter={filter}&full=true&config=true"; - var response = await _httpClient.GetAsync(searchUrl); + using var response = await _httpClient.GetAsync(searchUrl); var responseContent = await response.Content.ReadAsStringAsync(); try @@ -53,7 +53,7 @@ public static async Task GetContentsOfTextFile(string modelId, string fi { var url = $"{HFUrl}/{modelId}/resolve/{commitOrBranch}/{filePath}"; - var response = await _httpClient.GetAsync(url); + using var response = await _httpClient.GetAsync(url); return await response.Content.ReadAsStringAsync(); } @@ -68,7 +68,7 @@ public static async Task GetContentsOfTextFile(string fileUrl) var requestUrl = $"{HFUrl}/{url.Organization}/{url.Repo}/resolve/{url.Ref}/{url.Path}"; - var response = await _httpClient.GetAsync(requestUrl); + using var response = await _httpClient.GetAsync(requestUrl); return await response.Content.ReadAsStringAsync(); } } \ No newline at end of file diff --git a/AIDevGallery/Utils/ModelDownloadQueue.cs b/AIDevGallery/Utils/ModelDownloadQueue.cs index d07430e8..3004190b 100644 --- a/AIDevGallery/Utils/ModelDownloadQueue.cs +++ b/AIDevGallery/Utils/ModelDownloadQueue.cs @@ -88,7 +88,12 @@ public void CancelModelDownload(ModelDownload download) ModelDownloadCancelEvent.Log(download.Details.Url); _queue.Remove(download); ModelsChanged?.Invoke(this); + + // ModelDownload is managed by the queue and will be disposed when removed + // Don't dispose injected or externally managed instances +#pragma warning disable IDISP007 // Don't dispose injected download.Dispose(); +#pragma warning restore IDISP007 } public IReadOnlyList GetDownloads() From 234531c6887fd7fd6d5f37d35697b3eebdbbd921 Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Tue, 6 Jan 2026 12:10:52 +0800 Subject: [PATCH 12/23] format --- .../Controls/DefaultSVGRendererTests.cs | 138 ++------ .../Samples/ResourceLifecycleTests.cs | 310 +++--------------- .../Samples/WCRAPIs/ColoringBook.xaml.cs | 6 +- 3 files changed, 77 insertions(+), 377 deletions(-) diff --git a/AIDevGallery.Tests/UnitTests/Controls/DefaultSVGRendererTests.cs b/AIDevGallery.Tests/UnitTests/Controls/DefaultSVGRendererTests.cs index ab22fa51..45f7ebd2 100644 --- a/AIDevGallery.Tests/UnitTests/Controls/DefaultSVGRendererTests.cs +++ b/AIDevGallery.Tests/UnitTests/Controls/DefaultSVGRendererTests.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using CommunityToolkit.Labs.WinUI.MarkdownTextBlock; -using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media.Imaging; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; @@ -22,7 +21,7 @@ public void TestInitialize() } [TestMethod] - public async Task SvgToImage_WithValidSvgString_ReturnsImageWithSource() + public async Task SvgToImageWithValidSvgStringReturnsImageWithSource() { // Arrange var svgString = @" @@ -36,11 +35,11 @@ public async Task SvgToImage_WithValidSvgString_ReturnsImageWithSource() // Assert Assert.IsNotNull(image, "Image should not be null"); Assert.IsNotNull(image.Source, "Image.Source should not be null"); - Assert.IsInstanceOfType(image.Source, typeof(SvgImageSource), "Image.Source should be SvgImageSource"); + Assert.IsInstanceOfType(image.Source, "Image.Source should be SvgImageSource"); } [TestMethod] - public async Task SvgToImage_WithValidSvgString_SetsCorrectDimensions() + public async Task SvgToImageWithValidSvgStringSetsCorrectDimensions() { // Arrange var svgString = @" @@ -57,26 +56,23 @@ public async Task SvgToImage_WithValidSvgString_SetsCorrectDimensions() } [TestMethod] - public async Task SvgToImage_WithSvgWithoutDimensions_ReturnsImageWithoutDimensions() + public async Task SvgToImageStreamIsClosedAfterLoadImageSourceRemainsValid() { - // Arrange - var svgString = @" - - - "; - - // Act - var image = await _renderer!.SvgToImage(svgString); + // *** CRITICAL TEST FOR DefaultSVGRenderer.cs *** + // This test validates the stream lifecycle in DefaultSVGRenderer.SvgToImage: + // + // Pattern in DefaultSVGRenderer.cs: + // using (var randomAccessStream = await svgString.AsRandomAccessStream()) + // { + // await svgSource.SetSourceAsync(randomAccessStream); + // } // randomAccessStream is disposed here + // + // Key Question: Is it safe to dispose the stream after SetSourceAsync? + // Answer: YES, because SetSourceAsync copies the data synchronously. + // + // This test verifies that the SvgImageSource remains valid even after + // the stream is disposed, preventing image rendering failures. - // Assert - Assert.IsNotNull(image); - Assert.IsNotNull(image.Source); - // Width and Height should be 0 or default if not specified in SVG - } - - [TestMethod] - public async Task SvgToImage_StreamIsClosedAfterLoad_ImageSourceRemainsValid() - { // Arrange var svgString = @" @@ -86,84 +82,35 @@ public async Task SvgToImage_StreamIsClosedAfterLoad_ImageSourceRemainsValid() // Act var image = await _renderer!.SvgToImage(svgString); - // This test verifies that the using statement for randomAccessStream - // doesn't cause issues. The stream is closed after SetSourceAsync, - // but the image source should still be valid because SetSourceAsync - // copies the data synchronously. + // The using statement for randomAccessStream has already completed at this point. + // The stream is closed, but the image source should still be valid because + // SetSourceAsync copies the data synchronously during the call. - // Assert + // Assert - Image source should remain valid after stream is disposed Assert.IsNotNull(image.Source, "Image source should remain valid after stream is disposed"); - - // Additional verification: try to access properties + + // Additional verification: try to access and cast the source var svgSource = image.Source as SvgImageSource; Assert.IsNotNull(svgSource, "Should be able to cast to SvgImageSource"); - } - - [TestMethod] - public async Task SvgToImage_ComplexSvg_LoadsSuccessfully() - { - // Arrange - var complexSvg = @" - - - - - - - - - SVG Test - "; - - // Act - var image = await _renderer!.SvgToImage(complexSvg); - // Assert - Assert.IsNotNull(image); - Assert.IsNotNull(image.Source); - Assert.AreEqual(300.0, image.Width); - Assert.AreEqual(200.0, image.Height); - } - - [TestMethod] - public async Task SvgToImage_MultipleCallsInSequence_AllSucceed() - { - // This test verifies that multiple sequential calls work correctly - // and that resource disposal doesn't interfere with subsequent calls - - // Arrange - var svgStrings = new[] - { - @"", - @"", - @"" - }; - - // Act & Assert - foreach (var svgString in svgStrings) - { - var image = await _renderer!.SvgToImage(svgString); - Assert.IsNotNull(image); - Assert.IsNotNull(image.Source); - } + // Verify dimensions were parsed correctly + Assert.AreEqual(100.0, image.Width, "Width should be parsed from SVG"); + Assert.AreEqual(100.0, image.Height, "Height should be parsed from SVG"); } [TestMethod] - [ExpectedException(typeof(Exception))] - public async Task SvgToImage_WithInvalidSvg_ThrowsException() + public async Task SvgToImageWithInvalidSvgThrowsException() { // Arrange var invalidSvg = "This is not valid SVG"; - // Act - // This should throw an exception during SetSourceAsync - await _renderer!.SvgToImage(invalidSvg); - - // Assert is handled by ExpectedException + // Act & Assert - should throw an exception during SetSourceAsync + await Assert.ThrowsExactlyAsync(async () => + await _renderer!.SvgToImage(invalidSvg)); } [TestMethod] - public async Task SvgToImage_WithEmptySvg_HandlesGracefully() + public async Task SvgToImageWithEmptySvgHandlesGracefully() { // Arrange var emptySvg = @""; @@ -175,23 +122,4 @@ public async Task SvgToImage_WithEmptySvg_HandlesGracefully() Assert.IsNotNull(image); Assert.IsNotNull(image.Source); } - - [TestMethod] - public async Task SvgToImage_WithUnicodeCharacters_LoadsSuccessfully() - { - // Arrange - var svgWithUnicode = @" - - 你好世界 🌍 - "; - - // Act - var image = await _renderer!.SvgToImage(svgWithUnicode); - - // Assert - Assert.IsNotNull(image); - Assert.IsNotNull(image.Source); - Assert.AreEqual(200.0, image.Width); - Assert.AreEqual(100.0, image.Height); - } -} +} \ No newline at end of file diff --git a/AIDevGallery.Tests/UnitTests/Samples/ResourceLifecycleTests.cs b/AIDevGallery.Tests/UnitTests/Samples/ResourceLifecycleTests.cs index 6481bbb1..3bc7fa7e 100644 --- a/AIDevGallery.Tests/UnitTests/Samples/ResourceLifecycleTests.cs +++ b/AIDevGallery.Tests/UnitTests/Samples/ResourceLifecycleTests.cs @@ -1,13 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using AIDevGallery.Samples.SharedCode; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Net; -using System.Net.Http; using System.Threading.Tasks; using Windows.Graphics.Imaging; @@ -20,8 +15,12 @@ namespace AIDevGallery.Tests.UnitTests.Samples; public class BitmapLifecycleTests { [TestMethod] - public void SoftwareBitmap_Convert_CreatesNewCopy() + 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, @@ -35,52 +34,26 @@ public void SoftwareBitmap_Convert_CreatesNewCopy() BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); - // Assert + // 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"); - // Clean up - originalBitmap.Dispose(); - convertedBitmap.Dispose(); - } - - [TestMethod] - public void SoftwareBitmap_CanDisposeOriginalAfterConvert() - { - // This test verifies that disposing the original bitmap after Convert - // doesn't affect the converted bitmap (they are independent copies) - - // Arrange - var originalBitmap = new SoftwareBitmap( - BitmapPixelFormat.Rgba8, - 50, - 50, - BitmapAlphaMode.Premultiplied); - - // Act - var convertedBitmap = SoftwareBitmap.Convert( - originalBitmap, - BitmapPixelFormat.Bgra8, - BitmapAlphaMode.Premultiplied); - - // Dispose original + // Dispose original - this should NOT affect the converted bitmap originalBitmap.Dispose(); - // Assert - converted bitmap should still be usable - Assert.AreEqual(50, convertedBitmap.PixelWidth); - Assert.AreEqual(50, convertedBitmap.PixelHeight); - Assert.AreEqual(BitmapPixelFormat.Bgra8, convertedBitmap.BitmapPixelFormat); + // 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] - [ExpectedException(typeof(ObjectDisposedException))] - public void SoftwareBitmap_AccessAfterDispose_ThrowsException() + public void SoftwareBitmapAccessAfterDisposeThrowsException() { // Arrange var bitmap = new SoftwareBitmap( @@ -89,47 +62,33 @@ public void SoftwareBitmap_AccessAfterDispose_ThrowsException() 100, BitmapAlphaMode.Premultiplied); +#pragma warning disable IDISP016 // Don't use disposed instance - this is intentional for testing dispose behavior // Act bitmap.Dispose(); // Assert - accessing properties after dispose should throw - _ = bitmap.PixelWidth; // This should throw ObjectDisposedException + var disposedBitmap = bitmap; + Assert.ThrowsExactly(() => _ = disposedBitmap.PixelWidth); +#pragma warning restore IDISP016 } [TestMethod] - public void SoftwareBitmap_UsingPattern_DisposesCorrectly() + public async Task BackgroundRemoverBitmapLifecycleVerifyCorrectPattern() { - // This test verifies the using pattern works correctly - SoftwareBitmap? bitmapReference = null; - - // Act - using (var bitmap = new SoftwareBitmap( - BitmapPixelFormat.Bgra8, - 100, - 100, - BitmapAlphaMode.Premultiplied)) - { - bitmapReference = bitmap; - Assert.AreEqual(100, bitmap.PixelWidth); - } - - // Assert - bitmap should be disposed after using block - try - { - _ = bitmapReference!.PixelWidth; - Assert.Fail("Should have thrown ObjectDisposedException"); - } - catch (ObjectDisposedException) - { - // Expected - } - } - - [TestMethod] - public async Task SimulateBackgroundRemoverPattern_VerifyLifecycle() - { - // This test simulates the pattern used in BackgroundRemover.xaml.cs - // to verify that disposing outputBitmap after SetImageSource is safe + // *** 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; @@ -144,7 +103,7 @@ public async Task SimulateBackgroundRemoverPattern_VerifyLifecycle() BitmapAlphaMode.Premultiplied); // Act - Simulate SetImageSource logic - // This is what happens inside SetImageSource: + // Inside SetImageSource, this happens: convertedBitmap = SoftwareBitmap.Convert( outputBitmap, BitmapPixelFormat.Bgra8, @@ -152,212 +111,25 @@ public async Task SimulateBackgroundRemoverPattern_VerifyLifecycle() // At this point in BackgroundRemover, we would: // await bitmapSource.SetBitmapAsync(convertedBitmap); - // For testing, we just verify the converted bitmap is valid - - Assert.IsNotNull(convertedBitmap); - Assert.AreEqual(200, convertedBitmap.PixelWidth); + // 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 a copy - Assert.AreEqual(200, convertedBitmap.PixelWidth); - Assert.AreEqual(200, convertedBitmap.PixelHeight); + // 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 + // Clean up - in real code, the UI framework holds the convertedBitmap reference convertedBitmap?.Dispose(); } } - - [TestMethod] - public void MultipleConversions_EachCreateIndependentCopies() - { - // Arrange - var originalBitmap = new SoftwareBitmap( - BitmapPixelFormat.Rgba8, - 100, - 100, - BitmapAlphaMode.Premultiplied); - - // Act - Convert multiple times - var converted1 = SoftwareBitmap.Convert(originalBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); - var converted2 = SoftwareBitmap.Convert(originalBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); - var converted3 = SoftwareBitmap.Convert(originalBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); - - // Assert - All should be different instances - Assert.AreNotSame(converted1, converted2); - Assert.AreNotSame(converted2, converted3); - Assert.AreNotSame(converted1, converted3); - - // Dispose one shouldn't affect others - converted1.Dispose(); - Assert.AreEqual(100, converted2.PixelWidth); // Should still work - Assert.AreEqual(100, converted3.PixelWidth); // Should still work - - // Clean up - originalBitmap.Dispose(); - converted2.Dispose(); - converted3.Dispose(); - } -} - -/// -/// Performance tests for HttpClient usage patterns -/// Note: These are integration tests that make actual HTTP calls -/// Mark with [Ignore] if you want to skip them in regular test runs -/// -[TestClass] -public class HttpClientPerformanceTests -{ - private const string TestImageUrl = "https://via.placeholder.com/150"; - private const int WarmupIterations = 2; - private const int TestIterations = 10; - - [TestMethod] - [TestCategory("Performance")] - public async Task HttpClient_NewInstanceEachTime_MeasurePerformance() - { - // Warmup - for (int i = 0; i < WarmupIterations; i++) - { - using var client = new HttpClient(); - using var response = await client.GetAsync(TestImageUrl); - } - - // Measure - var stopwatch = Stopwatch.StartNew(); - for (int i = 0; i < TestIterations; i++) - { - using var client = new HttpClient(); - using var response = await client.GetAsync(TestImageUrl); - _ = await response.Content.ReadAsByteArrayAsync(); - } - stopwatch.Stop(); - - var avgTime = stopwatch.ElapsedMilliseconds / (double)TestIterations; - Debug.WriteLine($"New HttpClient each time - Average: {avgTime:F2}ms, Total: {stopwatch.ElapsedMilliseconds}ms"); - - // No assertion, just measuring - Assert.IsTrue(stopwatch.ElapsedMilliseconds > 0); - } - - [TestMethod] - [TestCategory("Performance")] - public async Task HttpClient_SharedInstance_MeasurePerformance() - { - var sharedClient = new HttpClient(); - - // Warmup - for (int i = 0; i < WarmupIterations; i++) - { - using var response = await sharedClient.GetAsync(TestImageUrl); - } - - // Measure - var stopwatch = Stopwatch.StartNew(); - for (int i = 0; i < TestIterations; i++) - { - using var response = await sharedClient.GetAsync(TestImageUrl); - _ = await response.Content.ReadAsByteArrayAsync(); - } - stopwatch.Stop(); - - var avgTime = stopwatch.ElapsedMilliseconds / (double)TestIterations; - Debug.WriteLine($"Shared HttpClient - Average: {avgTime:F2}ms, Total: {stopwatch.ElapsedMilliseconds}ms"); - - // No assertion, just measuring - Assert.IsTrue(stopwatch.ElapsedMilliseconds > 0); - - sharedClient.Dispose(); - } - - [TestMethod] - [TestCategory("Performance")] - public async Task HttpClient_ComparePerformance() - { - const int iterations = 5; - var results = new Dictionary(); - - // Test 1: New instance each time - var sw1 = Stopwatch.StartNew(); - for (int i = 0; i < iterations; i++) - { - using var client = new HttpClient(); - using var response = await client.GetAsync(TestImageUrl); - _ = await response.Content.ReadAsByteArrayAsync(); - } - sw1.Stop(); - results["NewInstance"] = sw1.ElapsedMilliseconds; - - // Test 2: Shared instance - var sharedClient = new HttpClient(); - var sw2 = Stopwatch.StartNew(); - for (int i = 0; i < iterations; i++) - { - using var response = await sharedClient.GetAsync(TestImageUrl); - _ = await response.Content.ReadAsByteArrayAsync(); - } - sw2.Stop(); - results["SharedInstance"] = sw2.ElapsedMilliseconds; - sharedClient.Dispose(); - - // Report results - var improvement = ((results["NewInstance"] - results["SharedInstance"]) / (double)results["NewInstance"]) * 100; - Debug.WriteLine($"\n=== HttpClient Performance Comparison ==="); - Debug.WriteLine($"New instance each time: {results["NewInstance"]}ms"); - Debug.WriteLine($"Shared instance: {results["SharedInstance"]}ms"); - Debug.WriteLine($"Performance improvement: {improvement:F1}%"); - Debug.WriteLine($"=========================================\n"); - - // Assert that shared instance is at least not worse - // (In reality, it should be significantly better) - Assert.IsTrue(results["SharedInstance"] <= results["NewInstance"] * 1.2, - $"Shared HttpClient should not be significantly slower. New: {results["NewInstance"]}ms, Shared: {results["SharedInstance"]}ms"); - } - - [TestMethod] - [TestCategory("Performance")] - [Ignore] // Ignore by default as it takes time - public async Task HttpClient_SocketExhaustion_Simulate() - { - // This test simulates the socket exhaustion problem - // by creating many HttpClient instances rapidly - - var tasks = new List(); - var errorCount = 0; - - try - { - for (int i = 0; i < 100; i++) - { - tasks.Add(Task.Run(async () => - { - try - { - using var client = new HttpClient(); - using var response = await client.GetAsync(TestImageUrl); - } - catch (HttpRequestException) - { - System.Threading.Interlocked.Increment(ref errorCount); - } - })); - } - - await Task.WhenAll(tasks); - - Debug.WriteLine($"Errors encountered: {errorCount}/100"); - - // We expect some errors due to socket exhaustion - // This demonstrates the problem with creating new HttpClient instances - } - catch (Exception ex) - { - Debug.WriteLine($"Exception: {ex.Message}"); - } - } -} +} \ No newline at end of file diff --git a/AIDevGallery/Samples/WCRAPIs/ColoringBook.xaml.cs b/AIDevGallery/Samples/WCRAPIs/ColoringBook.xaml.cs index 268413c8..77ac5996 100644 --- a/AIDevGallery/Samples/WCRAPIs/ColoringBook.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/ColoringBook.xaml.cs @@ -230,9 +230,9 @@ await Task.Run( #pragma warning disable IDISP003 // Dispose previous before re-assigning var outputBitmap = ApplyMaskWithPrompt(prompt, _inputBitmap, mask); if (_cts!.Token.IsCancellationRequested) - { - return; - } + { + return; + } if (outputBitmap != null) { From a284b13c15a85d88306694b8630a2d709e14fa06 Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Tue, 6 Jan 2026 12:14:35 +0800 Subject: [PATCH 13/23] typo --- AIDevGallery/Samples/WCRAPIs/KnowledgeRetrieval.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AIDevGallery/Samples/WCRAPIs/KnowledgeRetrieval.xaml.cs b/AIDevGallery/Samples/WCRAPIs/KnowledgeRetrieval.xaml.cs index 9bac2295..5d9bf3f3 100644 --- a/AIDevGallery/Samples/WCRAPIs/KnowledgeRetrieval.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/KnowledgeRetrieval.xaml.cs @@ -58,7 +58,7 @@ internal sealed partial class KnowledgeRetrieval : BaseSamplePage, IDisposable // This is some text data that we want to add to the index: private Dictionary simpleTextData = new Dictionary { - { "item1", "Preparing a hearty vegetable stew begins with chopping fresh carrots, onions, and celery. Saut� them in olive oil until fragrant, then add diced tomatoes, herbs, and vegetable broth. Simmer gently for an hour, allowing flavors to meld into a comforting dish perfect for cold evenings." }, + { "item1", "Preparing a hearty vegetable stew begins with chopping fresh carrots, onions, and celery. Sauté them in olive oil until fragrant, then add diced tomatoes, herbs, and vegetable broth. Simmer gently for an hour, allowing flavors to meld into a comforting dish perfect for cold evenings." }, { "item2", "Modern exhibition design combines narrative flow with spatial strategy. Lighting emphasizes focal objects while circulation paths avoid bottlenecks. Materials complement artifacts without visual competition. Interactive elements invite engagement but remain intuitive. Environmental controls protect sensitive works. Success balances scholarship, aesthetics, and visitor experience through thoughtful, cohesive design choices." }, { "item3", "Domestic cats communicate through posture, tail flicks, and vocalizations. Play mimics hunting behaviors like stalking and pouncing, supporting agility and mental stimulation. Scratching maintains claws and marks territory, so provide sturdy posts. Balanced diets, hydration, and routine veterinary care sustain health. Safe retreats and vertical spaces reduce stress and encourage exploration." }, { "item4", "Snowboarding across pristine slopes combines agility, balance, and speed. Riders carve smooth turns on powder, adjust stance for control, and master jumps in terrain parks. Essential gear includes boots, bindings, and helmets for safety. Embrace crisp alpine air while perfecting tricks and enjoying the thrill of winter adventure." }, From ee76e57e87826a454b3ba4a968dbfaa1017d85f0 Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Tue, 6 Jan 2026 12:25:03 +0800 Subject: [PATCH 14/23] remove --- .../Controls/DefaultSVGRendererTests.cs | 125 ------------------ 1 file changed, 125 deletions(-) delete mode 100644 AIDevGallery.Tests/UnitTests/Controls/DefaultSVGRendererTests.cs diff --git a/AIDevGallery.Tests/UnitTests/Controls/DefaultSVGRendererTests.cs b/AIDevGallery.Tests/UnitTests/Controls/DefaultSVGRendererTests.cs deleted file mode 100644 index 45f7ebd2..00000000 --- a/AIDevGallery.Tests/UnitTests/Controls/DefaultSVGRendererTests.cs +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using CommunityToolkit.Labs.WinUI.MarkdownTextBlock; -using Microsoft.UI.Xaml.Media.Imaging; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.Threading.Tasks; - -namespace AIDevGallery.Tests.UnitTests.Controls; - -[TestClass] -public class DefaultSVGRendererTests -{ - private DefaultSVGRenderer? _renderer; - - [TestInitialize] - public void TestInitialize() - { - _renderer = new DefaultSVGRenderer(); - } - - [TestMethod] - public async Task SvgToImageWithValidSvgStringReturnsImageWithSource() - { - // Arrange - var svgString = @" - - - "; - - // Act - var image = await _renderer!.SvgToImage(svgString); - - // Assert - Assert.IsNotNull(image, "Image should not be null"); - Assert.IsNotNull(image.Source, "Image.Source should not be null"); - Assert.IsInstanceOfType(image.Source, "Image.Source should be SvgImageSource"); - } - - [TestMethod] - public async Task SvgToImageWithValidSvgStringSetsCorrectDimensions() - { - // Arrange - var svgString = @" - - - "; - - // Act - var image = await _renderer!.SvgToImage(svgString); - - // Assert - Assert.AreEqual(200.0, image.Width, "Image width should match SVG width"); - Assert.AreEqual(150.0, image.Height, "Image height should match SVG height"); - } - - [TestMethod] - public async Task SvgToImageStreamIsClosedAfterLoadImageSourceRemainsValid() - { - // *** CRITICAL TEST FOR DefaultSVGRenderer.cs *** - // This test validates the stream lifecycle in DefaultSVGRenderer.SvgToImage: - // - // Pattern in DefaultSVGRenderer.cs: - // using (var randomAccessStream = await svgString.AsRandomAccessStream()) - // { - // await svgSource.SetSourceAsync(randomAccessStream); - // } // randomAccessStream is disposed here - // - // Key Question: Is it safe to dispose the stream after SetSourceAsync? - // Answer: YES, because SetSourceAsync copies the data synchronously. - // - // This test verifies that the SvgImageSource remains valid even after - // the stream is disposed, preventing image rendering failures. - - // Arrange - var svgString = @" - - - "; - - // Act - var image = await _renderer!.SvgToImage(svgString); - - // The using statement for randomAccessStream has already completed at this point. - // The stream is closed, but the image source should still be valid because - // SetSourceAsync copies the data synchronously during the call. - - // Assert - Image source should remain valid after stream is disposed - Assert.IsNotNull(image.Source, "Image source should remain valid after stream is disposed"); - - // Additional verification: try to access and cast the source - var svgSource = image.Source as SvgImageSource; - Assert.IsNotNull(svgSource, "Should be able to cast to SvgImageSource"); - - // Verify dimensions were parsed correctly - Assert.AreEqual(100.0, image.Width, "Width should be parsed from SVG"); - Assert.AreEqual(100.0, image.Height, "Height should be parsed from SVG"); - } - - [TestMethod] - public async Task SvgToImageWithInvalidSvgThrowsException() - { - // Arrange - var invalidSvg = "This is not valid SVG"; - - // Act & Assert - should throw an exception during SetSourceAsync - await Assert.ThrowsExactlyAsync(async () => - await _renderer!.SvgToImage(invalidSvg)); - } - - [TestMethod] - public async Task SvgToImageWithEmptySvgHandlesGracefully() - { - // Arrange - var emptySvg = @""; - - // Act - var image = await _renderer!.SvgToImage(emptySvg); - - // Assert - Assert.IsNotNull(image); - Assert.IsNotNull(image.Source); - } -} \ No newline at end of file From ec0aa0361e5e709071d31d289b9463a7d8a5c9e7 Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Tue, 6 Jan 2026 12:35:25 +0800 Subject: [PATCH 15/23] remove --- .../Samples/ResourceLifecycleTests.cs | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/AIDevGallery.Tests/UnitTests/Samples/ResourceLifecycleTests.cs b/AIDevGallery.Tests/UnitTests/Samples/ResourceLifecycleTests.cs index 3bc7fa7e..01463e67 100644 --- a/AIDevGallery.Tests/UnitTests/Samples/ResourceLifecycleTests.cs +++ b/AIDevGallery.Tests/UnitTests/Samples/ResourceLifecycleTests.cs @@ -52,26 +52,6 @@ public void SoftwareBitmapConvertCreatesIndependentCopy() convertedBitmap.Dispose(); } - [TestMethod] - public void SoftwareBitmapAccessAfterDisposeThrowsException() - { - // Arrange - var bitmap = new SoftwareBitmap( - BitmapPixelFormat.Bgra8, - 100, - 100, - BitmapAlphaMode.Premultiplied); - -#pragma warning disable IDISP016 // Don't use disposed instance - this is intentional for testing dispose behavior - // Act - bitmap.Dispose(); - - // Assert - accessing properties after dispose should throw - var disposedBitmap = bitmap; - Assert.ThrowsExactly(() => _ = disposedBitmap.PixelWidth); -#pragma warning restore IDISP016 - } - [TestMethod] public async Task BackgroundRemoverBitmapLifecycleVerifyCorrectPattern() { From 90a11ffa8808e381214a1b6bcf859a11d9e0130d Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Tue, 6 Jan 2026 13:40:51 +0800 Subject: [PATCH 16/23] fix --- AIDevGallery.Tests/UnitTests/Samples/ResourceLifecycleTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/AIDevGallery.Tests/UnitTests/Samples/ResourceLifecycleTests.cs b/AIDevGallery.Tests/UnitTests/Samples/ResourceLifecycleTests.cs index 01463e67..3e12a2f8 100644 --- a/AIDevGallery.Tests/UnitTests/Samples/ResourceLifecycleTests.cs +++ b/AIDevGallery.Tests/UnitTests/Samples/ResourceLifecycleTests.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; using System.Threading.Tasks; using Windows.Graphics.Imaging; From 83c66c09b05550903c2a0ed97f443cf5b68f9876 Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Tue, 6 Jan 2026 19:32:36 +0800 Subject: [PATCH 17/23] fix --- AIDevGallery/Controls/SampleContainer.xaml.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/AIDevGallery/Controls/SampleContainer.xaml.cs b/AIDevGallery/Controls/SampleContainer.xaml.cs index 8745a3a4..cfc76b1a 100644 --- a/AIDevGallery/Controls/SampleContainer.xaml.cs +++ b/AIDevGallery/Controls/SampleContainer.xaml.cs @@ -108,12 +108,21 @@ internal static async Task WaitUnloadAllAsync() private void CancelCTS() { - if (_sampleLoadingCts != null) +#pragma warning disable IDISP017 // Prefer using. - CancellationTokenSource is managed across method boundaries + var cts = _sampleLoadingCts; + if (cts != null) { - _sampleLoadingCts.Cancel(); - _sampleLoadingCts.Dispose(); _sampleLoadingCts = null; + try + { + cts.Cancel(); + } + finally + { + cts.Dispose(); + } } +#pragma warning restore IDISP017 // Prefer using. } public SampleContainer() From 05d68f81bf8fd7d9b1c307efb07424ecf3848110 Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Thu, 8 Jan 2026 19:27:23 +0800 Subject: [PATCH 18/23] Fix resource disposal issues - Remove duplicate cleanup code in SampleContainer finally block - Use static HttpClient in MyImage to prevent socket exhaustion - Remove premature disposal of AsRandomAccessStream wrapper in DefaultSVGRenderer --- AIDevGallery/Controls/Markdown/DefaultSVGRenderer.cs | 6 ++++-- AIDevGallery/Controls/Markdown/TextElements/MyImage.cs | 5 ++--- AIDevGallery/Controls/SampleContainer.xaml.cs | 4 ---- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/AIDevGallery/Controls/Markdown/DefaultSVGRenderer.cs b/AIDevGallery/Controls/Markdown/DefaultSVGRenderer.cs index 84c4d8d9..ab57f695 100644 --- a/AIDevGallery/Controls/Markdown/DefaultSVGRenderer.cs +++ b/AIDevGallery/Controls/Markdown/DefaultSVGRenderer.cs @@ -30,8 +30,10 @@ public async Task SvgToImage(string svgString) memoryStream.Position = 0; // Load the SVG from the MemoryStream - // AsRandomAccessStream() returns an IDisposable that's owned by the SvgImageSource after SetSourceAsync - using var randomAccessStream = memoryStream.AsRandomAccessStream(); + // AsRandomAccessStream() returns a wrapper that should not be disposed - it's owned by SvgImageSource after SetSourceAsync +#pragma warning disable IDISP001 // Dispose created + var randomAccessStream = memoryStream.AsRandomAccessStream(); +#pragma warning restore IDISP001 await svgImageSource.SetSourceAsync(randomAccessStream); } diff --git a/AIDevGallery/Controls/Markdown/TextElements/MyImage.cs b/AIDevGallery/Controls/Markdown/TextElements/MyImage.cs index 954201c0..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,10 +109,8 @@ private async void LoadImage(object sender, RoutedEventArgs e) } else { - using HttpClient client = new(); - // Download data from URL - using HttpResponseMessage response = await client.GetAsync(_uri); + using HttpResponseMessage response = await _httpClient.GetAsync(_uri); // Get the Content-Type header #pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. diff --git a/AIDevGallery/Controls/SampleContainer.xaml.cs b/AIDevGallery/Controls/SampleContainer.xaml.cs index cfc76b1a..9461f8c0 100644 --- a/AIDevGallery/Controls/SampleContainer.xaml.cs +++ b/AIDevGallery/Controls/SampleContainer.xaml.cs @@ -326,10 +326,6 @@ public async Task LoadSampleAsync(Sample? sample, List? models, Wi _sampleLoadingCts = null; } - _sampleLoadedCompletionSource = null; - _sampleLoadingCts?.Dispose(); - _sampleLoadingCts = null; - NavigatedToSampleLoadedEvent.Log(sample.Name ?? string.Empty); VisualStateManager.GoToState(this, "SampleLoaded", true); From 0e2494bceaf24e47665ba855c54b4a820ee15b5c Mon Sep 17 00:00:00 2001 From: Milly Way <181923419@qq.com> Date: Thu, 8 Jan 2026 19:29:31 +0800 Subject: [PATCH 19/23] Fix resource disposal issues (#551) - Remove duplicate cleanup code in SampleContainer finally block - Use static HttpClient in MyImage to prevent socket exhaustion - Remove premature disposal of AsRandomAccessStream wrapper in DefaultSVGRenderer Co-authored-by: Milly Wei (from Dev Box) --- AIDevGallery/Controls/Markdown/DefaultSVGRenderer.cs | 6 ++++-- AIDevGallery/Controls/Markdown/TextElements/MyImage.cs | 5 ++--- AIDevGallery/Controls/SampleContainer.xaml.cs | 4 ---- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/AIDevGallery/Controls/Markdown/DefaultSVGRenderer.cs b/AIDevGallery/Controls/Markdown/DefaultSVGRenderer.cs index 84c4d8d9..ab57f695 100644 --- a/AIDevGallery/Controls/Markdown/DefaultSVGRenderer.cs +++ b/AIDevGallery/Controls/Markdown/DefaultSVGRenderer.cs @@ -30,8 +30,10 @@ public async Task SvgToImage(string svgString) memoryStream.Position = 0; // Load the SVG from the MemoryStream - // AsRandomAccessStream() returns an IDisposable that's owned by the SvgImageSource after SetSourceAsync - using var randomAccessStream = memoryStream.AsRandomAccessStream(); + // AsRandomAccessStream() returns a wrapper that should not be disposed - it's owned by SvgImageSource after SetSourceAsync +#pragma warning disable IDISP001 // Dispose created + var randomAccessStream = memoryStream.AsRandomAccessStream(); +#pragma warning restore IDISP001 await svgImageSource.SetSourceAsync(randomAccessStream); } diff --git a/AIDevGallery/Controls/Markdown/TextElements/MyImage.cs b/AIDevGallery/Controls/Markdown/TextElements/MyImage.cs index 954201c0..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,10 +109,8 @@ private async void LoadImage(object sender, RoutedEventArgs e) } else { - using HttpClient client = new(); - // Download data from URL - using HttpResponseMessage response = await client.GetAsync(_uri); + using HttpResponseMessage response = await _httpClient.GetAsync(_uri); // Get the Content-Type header #pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. diff --git a/AIDevGallery/Controls/SampleContainer.xaml.cs b/AIDevGallery/Controls/SampleContainer.xaml.cs index cfc76b1a..9461f8c0 100644 --- a/AIDevGallery/Controls/SampleContainer.xaml.cs +++ b/AIDevGallery/Controls/SampleContainer.xaml.cs @@ -326,10 +326,6 @@ public async Task LoadSampleAsync(Sample? sample, List? models, Wi _sampleLoadingCts = null; } - _sampleLoadedCompletionSource = null; - _sampleLoadingCts?.Dispose(); - _sampleLoadingCts = null; - NavigatedToSampleLoadedEvent.Log(sample.Name ?? string.Empty); VisualStateManager.GoToState(this, "SampleLoaded", true); From 691106459bb2404338433fd1c30fa5f38d5e5dab Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 19:41:03 +0800 Subject: [PATCH 20/23] Clarify pragma warning comment for outputBitmap ownership transfer (#552) * Initial plan * Clarify pragma warning comment in MagicEraser The comment now correctly explains that outputBitmap's ownership is transferred to become the new _inputBitmap value, not to the old _inputBitmap. Co-authored-by: weiyuanyue <176483933+weiyuanyue@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: weiyuanyue <176483933+weiyuanyue@users.noreply.github.com> --- AIDevGallery/Samples/WCRAPIs/MagicEraser.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AIDevGallery/Samples/WCRAPIs/MagicEraser.xaml.cs b/AIDevGallery/Samples/WCRAPIs/MagicEraser.xaml.cs index d563d9e6..9fa15231 100644 --- a/AIDevGallery/Samples/WCRAPIs/MagicEraser.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/MagicEraser.xaml.cs @@ -190,7 +190,7 @@ private async void EraseObject_Click(object sender, RoutedEventArgs e) try { - // Ownership of outputBitmap is transferred to _inputBitmap, so it should not be disposed here + // outputBitmap's ownership is transferred to become the new _inputBitmap, so it should not be disposed here #pragma warning disable IDISP001 // Dispose created var outputBitmap = _eraser.RemoveFromSoftwareBitmap(_inputBitmap, _maskBitmap); #pragma warning restore IDISP001 From 86af1be4e0a4520e5161ddb736a0f1842cd16f5c Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Thu, 8 Jan 2026 23:05:09 +0800 Subject: [PATCH 21/23] Fix copilot comments --- .../HomePage/Header/Lights/HoverLight.cs | 2 +- .../Controls/Markdown/DefaultSVGRenderer.cs | 4 ++- AIDevGallery/Controls/OpacityMask.xaml.cs | 12 ++++--- AIDevGallery/Controls/SampleContainer.xaml.cs | 31 ++++++++++--------- AIDevGallery/Controls/Shimmer.xaml.cs | 1 - .../Samples/SharedCode/BitmapFunctions.cs | 6 ++-- .../SharedCode/Controls/SmartPasteForm.cs | 7 +++-- .../Samples/WCRAPIs/ColoringBook.xaml.cs | 14 +++++++-- .../WCRAPIs/ForegroundExtractor.xaml.cs | 14 +++++++-- .../Samples/WCRAPIs/ImageDescription.xaml.cs | 10 ++++++ .../Samples/WCRAPIs/MagicEraser.xaml.cs | 9 ++++++ .../Samples/WCRAPIs/PhiSilicaLoRa.xaml.cs | 10 +++++- 12 files changed, 87 insertions(+), 33 deletions(-) diff --git a/AIDevGallery/Controls/HomePage/Header/Lights/HoverLight.cs b/AIDevGallery/Controls/HomePage/Header/Lights/HoverLight.cs index 62e95b94..e2dec2fd 100644 --- a/AIDevGallery/Controls/HomePage/Header/Lights/HoverLight.cs +++ b/AIDevGallery/Controls/HomePage/Header/Lights/HoverLight.cs @@ -128,7 +128,7 @@ public void Dispose() _offsetAnimation?.Dispose(); _hoverPosition?.Dispose(); _spotLight?.Dispose(); - CompositionLight?.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 ab57f695..df040c4d 100644 --- a/AIDevGallery/Controls/Markdown/DefaultSVGRenderer.cs +++ b/AIDevGallery/Controls/Markdown/DefaultSVGRenderer.cs @@ -30,7 +30,9 @@ public async Task SvgToImage(string svgString) memoryStream.Position = 0; // Load the SVG from the MemoryStream - // AsRandomAccessStream() returns a wrapper that should not be disposed - it's owned by SvgImageSource after SetSourceAsync + // 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 diff --git a/AIDevGallery/Controls/OpacityMask.xaml.cs b/AIDevGallery/Controls/OpacityMask.xaml.cs index 6d8748a9..3edc349e 100644 --- a/AIDevGallery/Controls/OpacityMask.xaml.cs +++ b/AIDevGallery/Controls/OpacityMask.xaml.cs @@ -31,9 +31,6 @@ public sealed partial class OpacityMaskView : ContentControl, IDisposable private const string MaskContainerTemplateName = "PART_MaskContainer"; private const string RootGridTemplateName = "PART_RootGrid"; -#pragma warning disable IDISP002 // Compositor is provided by the system and should not be disposed - private readonly Compositor _compositor = CompositionTarget.GetCompositorForCurrentThread(); -#pragma warning restore IDISP002 private CompositionBrush? _mask; private CompositionMaskBrush? _maskBrush; private SpriteVisual? _redirectVisual; @@ -69,8 +66,13 @@ protected override void OnApplyTemplate() ContentPresenter contentPresenter = (ContentPresenter)GetTemplateChild(ContentPresenterTemplateName); Border maskContainer = (Border)GetTemplateChild(MaskContainerTemplateName); + // 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(); + _maskBrush = compositor.CreateMaskBrush(); _contentVisualSurface?.Dispose(); _contentSizeAnimation?.Dispose(); _maskBrush.Source = GetVisualBrush(contentPresenter, ref _contentVisualSurface, ref _contentSizeAnimation); @@ -81,7 +83,7 @@ protected override void OnApplyTemplate() _maskBrush.Mask = OpacityMask is null ? null : _mask; _redirectVisual?.Dispose(); - _redirectVisual = _compositor.CreateSpriteVisual(); + _redirectVisual = compositor.CreateSpriteVisual(); _redirectVisual.RelativeSizeAdjustment = Vector2.One; _redirectVisual.Brush = _maskBrush; ElementCompositionPreview.SetElementChildVisual(rootGrid, _redirectVisual); diff --git a/AIDevGallery/Controls/SampleContainer.xaml.cs b/AIDevGallery/Controls/SampleContainer.xaml.cs index 9461f8c0..ccd2ac21 100644 --- a/AIDevGallery/Controls/SampleContainer.xaml.cs +++ b/AIDevGallery/Controls/SampleContainer.xaml.cs @@ -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,21 +109,17 @@ internal static async Task WaitUnloadAllAsync() private void CancelCTS() { -#pragma warning disable IDISP017 // Prefer using. - CancellationTokenSource is managed across method boundaries var cts = _sampleLoadingCts; - if (cts != null) + if (cts == null) { - _sampleLoadingCts = null; - try - { - cts.Cancel(); - } - finally - { - cts.Dispose(); - } + return; + } + + _sampleLoadingCts = null; + using (cts) + { + cts.Cancel(); } -#pragma warning restore IDISP017 // Prefer using. } public SampleContainer() @@ -132,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) { @@ -599,6 +596,12 @@ private void FooterGrid_SizeChanged(object sender, SizeChangedEventArgs e) public void Dispose() { - _sampleLoadingCts?.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 08c7dea9..6f6e74c9 100644 --- a/AIDevGallery/Controls/Shimmer.xaml.cs +++ b/AIDevGallery/Controls/Shimmer.xaml.cs @@ -105,7 +105,6 @@ private void OnLoaded(object sender, RoutedEventArgs e) private void OnUnloaded(object sender, RoutedEventArgs e) { ActualThemeChanged -= OnActualThemeChanged; - Dispose(); } private void OnActualThemeChanged(FrameworkElement sender, object args) diff --git a/AIDevGallery/Samples/SharedCode/BitmapFunctions.cs b/AIDevGallery/Samples/SharedCode/BitmapFunctions.cs index e63b5272..221eb761 100644 --- a/AIDevGallery/Samples/SharedCode/BitmapFunctions.cs +++ b/AIDevGallery/Samples/SharedCode/BitmapFunctions.cs @@ -96,8 +96,10 @@ public static async Task ResizeVideoFrameWithPadding(VideoFrame videoFra stream.Seek(0); // Reset stream position // Convert to System.Drawing.Bitmap - // IDISP: AsStream() returns a wrapper view of the same stream, disposing it here would close the underlying stream prematurely -#pragma warning disable IDISP004 // Don't ignore created IDisposable + // AsStream() returns a Stream wrapper over the IRandomAccessStream - we intentionally don't capture it + // The wrapper should not be disposed separately; it's valid as long as the underlying 'stream' is alive + // We do dispose the Bitmap in the using statement after we're done using it +#pragma warning disable IDISP004 // Don't ignore created IDisposable - AsStream() wrapper is intentionally not captured using var tempBitmap = new Bitmap(stream.AsStream()); #pragma warning restore IDISP004 Bitmap paddedBitmap = new(targetWidth, targetHeight); diff --git a/AIDevGallery/Samples/SharedCode/Controls/SmartPasteForm.cs b/AIDevGallery/Samples/SharedCode/Controls/SmartPasteForm.cs index c321df16..86be5908 100644 --- a/AIDevGallery/Samples/SharedCode/Controls/SmartPasteForm.cs +++ b/AIDevGallery/Samples/SharedCode/Controls/SmartPasteForm.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; @@ -104,7 +105,7 @@ private async Task> InferPasteValues(string clipboard } SampleInteractionEvent.SendSampleInteractedEvent(model, ScenarioType.SmartControlsSmartPaste, "InferPasteValues"); // - string outputMessage = string.Empty; + StringBuilder outputMessage = new(); PromptInput input = new() { Labels = fieldLabels, @@ -124,9 +125,9 @@ private async Task> InferPasteValues(string clipboard null, cts.Token)) { - outputMessage += messagePart; + outputMessage.Append(messagePart); - Match match = Regex.Match(outputMessage, "{([^}]*)}", RegexOptions.Multiline); + Match match = Regex.Match(outputMessage.ToString(), "{([^}]*)}", RegexOptions.Multiline); if (match.Success) { output = match.Value; diff --git a/AIDevGallery/Samples/WCRAPIs/ColoringBook.xaml.cs b/AIDevGallery/Samples/WCRAPIs/ColoringBook.xaml.cs index 77ac5996..59969420 100644 --- a/AIDevGallery/Samples/WCRAPIs/ColoringBook.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/ColoringBook.xaml.cs @@ -47,14 +47,21 @@ internal sealed partial class ColoringBook : BaseSamplePage, IDisposable private PointInt32? _selectionPoint; private CancellationTokenSource? _cts; private bool _isProgressVisible; + private bool _disposed; public ColoringBook() { + this.Unloaded += (s, e) => Dispose(); this.InitializeComponent(); } public void Dispose() { + if (_disposed) + { + return; + } + while (_bitmaps.Count > 0) { _bitmaps.Pop()?.Dispose(); @@ -63,6 +70,8 @@ public void Dispose() _inputBitmap?.Dispose(); _generator?.Dispose(); _cts?.Dispose(); + + _disposed = true; } protected override async Task LoadModelAsync(SampleNavigationParameters sampleParams) @@ -225,7 +234,8 @@ await Task.Run( using (mask) { - // Ownership of outputBitmap is transferred to _inputBitmap, so it should not be disposed here + // IDISP001: outputBitmap ownership is transferred to _inputBitmap, so it should not be disposed here + // IDISP003: Previous _inputBitmap is intentionally NOT disposed because it's saved to _bitmaps stack for undo #pragma warning disable IDISP001 // Dispose created #pragma warning disable IDISP003 // Dispose previous before re-assigning var outputBitmap = ApplyMaskWithPrompt(prompt, _inputBitmap, mask); @@ -237,7 +247,7 @@ await Task.Run( if (outputBitmap != null) { SetImageSource(outputBitmap); - _bitmaps.Push(_inputBitmap); + _bitmaps.Push(_inputBitmap); // Save for undo - will be disposed later when popped or in Dispose() _inputBitmap = outputBitmap; SwitchInputOutputView(); } diff --git a/AIDevGallery/Samples/WCRAPIs/ForegroundExtractor.xaml.cs b/AIDevGallery/Samples/WCRAPIs/ForegroundExtractor.xaml.cs index 16f35cd3..45d37600 100644 --- a/AIDevGallery/Samples/WCRAPIs/ForegroundExtractor.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/ForegroundExtractor.xaml.cs @@ -34,17 +34,26 @@ internal sealed partial class ForegroundExtractor : BaseSamplePage, IDisposable private ImageForegroundExtractor? _foregroundExtractor; private SoftwareBitmap? _inputBitmap; private SoftwareBitmap? _outputBitmap; + private bool _disposed; public ForegroundExtractor() { + this.Unloaded += (s, e) => Dispose(); this.InitializeComponent(); } public void Dispose() { + if (_disposed) + { + return; + } + _foregroundExtractor?.Dispose(); _inputBitmap?.Dispose(); _outputBitmap?.Dispose(); + + _disposed = true; } protected override async Task LoadModelAsync(SampleNavigationParameters sampleParams) @@ -181,9 +190,8 @@ private async Task SetImage(Image image, SoftwareBitmap? bitmap) return; } - // Dispose previous source to avoid memory leaks - // Image.Source is managed by this class, not injected, so disposal is appropriate -#pragma warning disable IDISP007 // Don't dispose injected - Image.Source is managed by this class + // Dispose previous source created by this method to avoid memory leaks +#pragma warning disable IDISP007 // Don't dispose injected - image.Source is created and managed by this method if (image.Source is SoftwareBitmapSource previousSource) { previousSource.Dispose(); diff --git a/AIDevGallery/Samples/WCRAPIs/ImageDescription.xaml.cs b/AIDevGallery/Samples/WCRAPIs/ImageDescription.xaml.cs index cf1f8098..ef2bbefd 100644 --- a/AIDevGallery/Samples/WCRAPIs/ImageDescription.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/ImageDescription.xaml.cs @@ -49,16 +49,26 @@ internal sealed partial class ImageDescription : BaseSamplePage, IDisposable private CancellationTokenSource? _cts; private SoftwareBitmap? _currentBitmap; private ImageDescriptionKind _currentKind = ImageDescriptionKind.BriefDescription; + private bool _disposed; public ImageDescription() { + this.Unloaded += (s, e) => Dispose(); this.InitializeComponent(); } public void Dispose() { + if (_disposed) + { + return; + } + _imageDescriptor?.Dispose(); _cts?.Dispose(); + _currentBitmap?.Dispose(); + + _disposed = true; } protected override async Task LoadModelAsync(SampleNavigationParameters sampleParams) diff --git a/AIDevGallery/Samples/WCRAPIs/MagicEraser.xaml.cs b/AIDevGallery/Samples/WCRAPIs/MagicEraser.xaml.cs index d563d9e6..4ef569cb 100644 --- a/AIDevGallery/Samples/WCRAPIs/MagicEraser.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/MagicEraser.xaml.cs @@ -40,14 +40,21 @@ internal sealed partial class MagicEraser : BaseSamplePage, IDisposable private bool _isDragging; private ImageObjectRemover? _eraser; private Stack _bitmaps = new(); + private bool _disposed; public MagicEraser() { + this.Unloaded += (s, e) => Dispose(); this.InitializeComponent(); } public void Dispose() { + if (_disposed) + { + return; + } + while (_bitmaps.Count > 0) { _bitmaps.Pop()?.Dispose(); @@ -56,6 +63,8 @@ public void Dispose() _inputBitmap?.Dispose(); _maskBitmap?.Dispose(); _eraser?.Dispose(); + + _disposed = true; } protected override async Task LoadModelAsync(SampleNavigationParameters sampleParams) diff --git a/AIDevGallery/Samples/WCRAPIs/PhiSilicaLoRa.xaml.cs b/AIDevGallery/Samples/WCRAPIs/PhiSilicaLoRa.xaml.cs index af1dc4b6..1cbf0704 100644 --- a/AIDevGallery/Samples/WCRAPIs/PhiSilicaLoRa.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/PhiSilicaLoRa.xaml.cs @@ -52,6 +52,7 @@ internal enum GenerationType private string _adapterFilePath = string.Empty; private string _systemPrompt = string.Empty; private GenerationType _generationType = GenerationType.All; + private bool _disposed; public PhiSilicaLoRa() { @@ -142,14 +143,21 @@ private async void CleanUp() App.AppData.LastSystemPrompt = SystemPromptBox.Text; // App.AppData.LastAdapterPath = _adapterFilePath; // await App.AppData.SaveAsync(); // - _languageModel?.Dispose(); + Dispose(); } public void Dispose() { + if (_disposed) + { + return; + } + _languageModel?.Dispose(); _loraModel?.Dispose(); _cts?.Dispose(); + + _disposed = true; } public bool IsProgressVisible From d8e161f936adb4331aa73839e4fc66df44847c66 Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Fri, 9 Jan 2026 00:36:50 +0800 Subject: [PATCH 22/23] Refactor: Replace scattered IDISP004 pragmas with ProcessHelper utility class - Created ProcessHelper.cs with OpenUrl() and OpenFolder() methods - Centralized all Process.Start calls for external processes - Replaced 16+ scattered #pragma warning disable IDISP004 with a single location - Fixed null reference warning in ModelSelectionControl - Removed unnecessary System.Diagnostics using statements - IDISP004 rule still active for other IDisposable types --- .../ModelPicker/AddHFModelView.xaml.cs | 10 +---- .../ModelPickerViews/OllamaPickerView.xaml.cs | 11 +---- .../ModelPickerViews/OnnxPickerView.xaml.cs | 42 +++---------------- .../ModelPickerViews/OpenAIPickerView.xaml.cs | 11 +---- .../Controls/ModelSelectionControl.xaml.cs | 34 +++------------ AIDevGallery/Helpers/ProcessHelper.cs | 41 ++++++++++++++++++ AIDevGallery/Pages/APIs/APIPage.xaml.cs | 10 +---- AIDevGallery/Pages/Models/ModelPage.xaml.cs | 26 ++---------- AIDevGallery/Pages/SettingsPage.xaml.cs | 12 ++---- .../WCRAPIs/KnowledgeRetrieval.xaml.cs | 2 + Directory.Build.props | 3 -- 11 files changed, 66 insertions(+), 136 deletions(-) create mode 100644 AIDevGallery/Helpers/ProcessHelper.cs diff --git a/AIDevGallery/Controls/ModelPicker/AddHFModelView.xaml.cs b/AIDevGallery/Controls/ModelPicker/AddHFModelView.xaml.cs index 63cf1e0c..e9a80831 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; @@ -260,14 +259,7 @@ private void Hyperlink_Click(object sender, RoutedEventArgs e) string url = result!.License.LicenseUrl ?? $"https://huggingface.co/{result.SearchResult.Id}"; - // 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 + 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 1ad00def..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,14 +57,7 @@ private void OllamaViewModelDetails_Click(object sender, RoutedEventArgs e) return; } - // 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 = modelDetailsUrl, - UseShellExecute = true - }); -#pragma warning restore IDISP004 + ProcessHelper.OpenUrl(modelDetailsUrl); } } diff --git a/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OnnxPickerView.xaml.cs b/AIDevGallery/Controls/ModelPicker/ModelPickerViews/OnnxPickerView.xaml.cs index 3920de52..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,10 +226,7 @@ private void OpenModelFolder_Click(object sender, RoutedEventArgs e) OpenModelFolderEvent.Log(cachedModel.Url); - // 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", path!); -#pragma warning restore IDISP004 + ProcessHelper.OpenFolder(path!); } } } @@ -305,14 +301,7 @@ private void ViewLicense_Click(object sender, RoutedEventArgs e) licenseUrl = details.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 = licenseUrl, - UseShellExecute = true - }); -#pragma warning restore IDISP004 + ProcessHelper.OpenUrl(licenseUrl); } } @@ -389,25 +378,11 @@ private void OpenAIToolkitButton_Click(object sender, RoutedEventArgs e) bool wasDeeplinkSuccessful = true; try { - // Process.Start for external process doesn't need disposal - process lifecycle is independent -#pragma warning disable IDISP004 // Don't ignore created IDisposable - Process.Start(new ProcessStartInfo() - { - FileName = toolkitDeeplink, - UseShellExecute = true - }); -#pragma warning restore IDISP004 + ProcessHelper.OpenUrl(toolkitDeeplink); } catch { - // 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 = "https://learn.microsoft.com/en-us/windows/ai/toolkit/", - UseShellExecute = true - }); -#pragma warning restore IDISP004 + ProcessHelper.OpenUrl("https://learn.microsoft.com/en-us/windows/ai/toolkit/"); wasDeeplinkSuccessful = false; } finally @@ -418,14 +393,7 @@ private void OpenAIToolkitButton_Click(object sender, RoutedEventArgs e) private void ViewDocumentationButton_Click(object sender, RoutedEventArgs e) { - // 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 = "https://aka.ms/winml-gallery-tutorial", - UseShellExecute = true - }); -#pragma warning restore IDISP004 + 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 de579fb4..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,14 +71,7 @@ private void ViewModelDetails_Click(object sender, RoutedEventArgs e) return; } - // 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 = modelDetailsUrl, - UseShellExecute = true - }); -#pragma warning restore IDISP004 + ProcessHelper.OpenUrl(modelDetailsUrl); } } diff --git a/AIDevGallery/Controls/ModelSelectionControl.xaml.cs b/AIDevGallery/Controls/ModelSelectionControl.xaml.cs index df492599..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,10 +322,7 @@ private void OpenModelFolder_Click(object sender, RoutedEventArgs e) OpenModelFolderEvent.Log(cachedModel.Url); - // 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", path!); -#pragma warning restore IDISP004 + ProcessHelper.OpenFolder(path!); } } } @@ -384,14 +380,10 @@ private void ModelCard_Click(object sender, RoutedEventArgs e) } } - // 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() + if (!string.IsNullOrEmpty(modelcardUrl)) { - FileName = modelcardUrl, - UseShellExecute = true - }); -#pragma warning restore IDISP004 + ProcessHelper.OpenUrl(modelcardUrl); + } } } } @@ -437,14 +429,7 @@ private void ViewLicense_Click(object sender, RoutedEventArgs e) licenseUrl = details.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 = licenseUrl, - UseShellExecute = true - }); -#pragma warning restore IDISP004 + ProcessHelper.OpenUrl(licenseUrl); } } @@ -614,14 +599,7 @@ private void ViewModelDetails_Click(object sender, RoutedEventArgs e) return; } - // 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 = modelDetailsUrl, - UseShellExecute = true - }); -#pragma warning restore IDISP004 + ProcessHelper.OpenUrl(modelDetailsUrl); } } } \ No newline at end of file diff --git a/AIDevGallery/Helpers/ProcessHelper.cs b/AIDevGallery/Helpers/ProcessHelper.cs new file mode 100644 index 00000000..714f05f9 --- /dev/null +++ b/AIDevGallery/Helpers/ProcessHelper.cs @@ -0,0 +1,41 @@ +// 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) + { + // 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) + { + // 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 e32569b5..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; @@ -185,14 +184,7 @@ private void MarkdownTextBlock_OnLinkClicked(object sender, CommunityToolkit.Lab ModelDetailsLinkClickedEvent.Log(link); - // 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 = link, - UseShellExecute = true - }); -#pragma warning restore IDISP004 + 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 675f2569..f52d787c 100644 --- a/AIDevGallery/Pages/Models/ModelPage.xaml.cs +++ b/AIDevGallery/Pages/Models/ModelPage.xaml.cs @@ -254,26 +254,13 @@ private void ToolkitActionFlyoutItem_Click(object sender, RoutedEventArgs e) bool wasDeeplinkSuccesful = true; try { - // Process.Start for external process doesn't need disposal - process lifecycle is independent -#pragma warning disable IDISP004 // Don't ignore created IDisposable - Process.Start(new ProcessStartInfo() - { - FileName = toolkitDeeplink, - UseShellExecute = true - }); -#pragma warning restore IDISP004 // Don't ignore created IDisposable + ProcessHelper.OpenUrl(toolkitDeeplink); } catch { try { -#pragma warning disable IDISP004 // Don't ignore created IDisposable - Process.Start(new ProcessStartInfo() - { - FileName = "https://learn.microsoft.com/en-us/windows/ai/toolkit/", - UseShellExecute = true - }); -#pragma warning restore IDISP004 // Don't ignore created IDisposable + ProcessHelper.OpenUrl("https://learn.microsoft.com/en-us/windows/ai/toolkit/"); } catch (Exception ex) { @@ -333,14 +320,7 @@ private void MarkdownTextBlock_OnLinkClicked(object sender, CommunityToolkit.Lab { try { - var psi = new ProcessStartInfo - { - FileName = uri.AbsoluteUri, - UseShellExecute = true - }; -#pragma warning disable IDISP004 // Don't ignore created IDisposable - Process.Start(psi); -#pragma warning restore IDISP004 // Don't ignore created IDisposable + 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 bb2cf151..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,10 +95,7 @@ private void FolderPathTxt_Click(object sender, RoutedEventArgs e) { if (cacheFolderPath != null) { - // 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", cacheFolderPath); -#pragma warning restore IDISP004 + ProcessHelper.OpenFolder(cacheFolderPath); } } @@ -204,10 +201,7 @@ private void ModelFolder_Click(object sender, RoutedEventArgs e) if (path != null && Directory.Exists(path)) { - // 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", path); -#pragma warning restore IDISP004 + ProcessHelper.OpenFolder(path); } } } diff --git a/AIDevGallery/Samples/WCRAPIs/KnowledgeRetrieval.xaml.cs b/AIDevGallery/Samples/WCRAPIs/KnowledgeRetrieval.xaml.cs index 5d9bf3f3..79928dbb 100644 --- a/AIDevGallery/Samples/WCRAPIs/KnowledgeRetrieval.xaml.cs +++ b/AIDevGallery/Samples/WCRAPIs/KnowledgeRetrieval.xaml.cs @@ -152,6 +152,8 @@ private void CleanUp() _indexer?.RemoveAll(); _indexer?.Dispose(); _indexer = null; + cts?.Dispose(); + cts = null; } public void Dispose() diff --git a/Directory.Build.props b/Directory.Build.props index 18aaea7e..0f9c7d8e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,9 +11,6 @@ Recommended true true - - - $(NoWarn);IDISP001;IDISP003;IDISP017 true From cc0c5e9e883993b54acc105b764c30e14ef2f512 Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Mon, 12 Jan 2026 19:18:01 +0800 Subject: [PATCH 23/23] Fixed --- AIDevGallery/Controls/OpacityMask.xaml.cs | 8 ++++++++ AIDevGallery/Controls/Shimmer.xaml.cs | 4 ---- AIDevGallery/Helpers/ProcessHelper.cs | 22 ++++++++++++++-------- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/AIDevGallery/Controls/OpacityMask.xaml.cs b/AIDevGallery/Controls/OpacityMask.xaml.cs index 3edc349e..66c2fb1e 100644 --- a/AIDevGallery/Controls/OpacityMask.xaml.cs +++ b/AIDevGallery/Controls/OpacityMask.xaml.cs @@ -38,6 +38,7 @@ public sealed partial class OpacityMaskView : ContentControl, IDisposable private ExpressionAnimation? _contentSizeAnimation; private CompositionVisualSurface? _maskVisualSurface; private ExpressionAnimation? _maskSizeAnimation; + private bool _disposed; /// /// Initializes a new instance of the class. @@ -126,6 +127,11 @@ private static void OnOpacityMaskChanged(DependencyObject d, DependencyPropertyC /// public void Dispose() { + if (_disposed) + { + return; + } + _contentVisualSurface?.Dispose(); _contentSizeAnimation?.Dispose(); _maskVisualSurface?.Dispose(); @@ -133,5 +139,7 @@ public void Dispose() _mask?.Dispose(); _maskBrush?.Dispose(); _redirectVisual?.Dispose(); + + _disposed = true; } } \ No newline at end of file diff --git a/AIDevGallery/Controls/Shimmer.xaml.cs b/AIDevGallery/Controls/Shimmer.xaml.cs index 6f6e74c9..86a38392 100644 --- a/AIDevGallery/Controls/Shimmer.xaml.cs +++ b/AIDevGallery/Controls/Shimmer.xaml.cs @@ -249,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; } diff --git a/AIDevGallery/Helpers/ProcessHelper.cs b/AIDevGallery/Helpers/ProcessHelper.cs index 714f05f9..ea8b8244 100644 --- a/AIDevGallery/Helpers/ProcessHelper.cs +++ b/AIDevGallery/Helpers/ProcessHelper.cs @@ -17,14 +17,17 @@ internal static class ProcessHelper /// The URL to open. public static void OpenUrl(string 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() + if (!string.IsNullOrWhiteSpace(url)) { - FileName = url, - UseShellExecute = true - }); + // 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 + } } /// @@ -33,9 +36,12 @@ public static void OpenUrl(string url) /// The folder path to open in Explorer. public static void OpenFolder(string folderPath) { - // Process.Start for explorer doesn't need disposal - process lifecycle is independent + 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); + Process.Start("explorer.exe", folderPath); #pragma warning restore IDISP004 + } } } \ No newline at end of file