View Issue Details

IDProjectCategoryView StatusLast Update
0003128NoesisGUIC# SDKpublic2024-02-29 10:37
Reportersamc Assigned Tomaherne  
PrioritynormalSeverityminor 
Status resolvedResolutionfixed 
Product Version3.2.3 
Target Version3.2.4 
Summary0003128: Exceptions in visual studio blend designer
Description

More details here:
https://www.noesisengine.com/forums/viewtopic.php?p=16709#p16709

There are exceptions when ProvideValue is called for LocExtension when a template it is in is being loaded, which then trickle down to the designer.

PlatformWindows

Activities

maherne

maherne

2024-02-27 18:49

developer   ~0009245

Thanks for the report.

I have attached a WPF LocExtension.cs with the fix, if you'd like to test it locally, and we will update the NoesisGUI.Extensions Nuget package with this fix in the next 24 hours.

LocExtension.cs (9,673 bytes)   
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Markup;

namespace NoesisGUIExtensions
{
    /// <summary>
    /// Implements a markup extension that supports references to a localization ResourceDictionary.
    ///
    /// Provides a value for any XAML property attribute by looking up a reference in the
    /// ResourceDictionary defined by the Source attached property. Values will be re-evaluated when
    /// the Source attached property changes.
    ///
    /// If used with a string or object property, and the provided resource key is not found, the
    /// LocExtension will return a string in the format "<Loc !%s>" where %s is replaced with the key.
    ///
    /// This example shows the full setup for a LocExtension. It utilizes the RichText attached
    /// property to support BBCode markup in the localized strings. The Loc.Source property references
    /// the "Language_en-gb.xaml" ResourceDictionary below.
    ///
    /// Usage:
    ///
    ///    <StackPanel
    ///      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    ///      xmlns:noesis="clr-namespace:NoesisGUIExtensions"
    ///      noesis:Loc.Source="Language_en-gb.xaml">
    ///      <Image Source="{noesis:Loc Flag}"/>
    ///      <TextBlock noesis:RichText.Text="{noesis:Loc TitleLabel}"/>
    ///      <TextBlock noesis:RichText.Text="{noesis:Loc SoundLabel}"/>
    ///    </StackPanel>
    /// 
    /// This is the contents of a "Language_en-gb.xaml" localized ResourceDictionary:
    ///
    /// Usage:
    ///
    ///    <ResourceDictionary
    ///      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    ///      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    ///      xmlns:sys="clr-namespace:System;assembly=mscorlib">
    ///      <ImageBrush x:Key="Flag" ImageSource="Flag_en-gb.png" Stretch="Fill"/>
    ///      <sys:String x:Key="TitleLabel">[b]LOCALIZATION SAMPLE[/b]</sys:String>
    ///      <sys:String x:Key="SoundLabel">A [i]sound[/i] label</sys:String>
    ///    </ResourceDictionary>
    ///
    /// </summary>
    [ContentProperty("ResourceKey")]
    public class LocExtension : MarkupExtension
    {
        private string _resourceKey;

        public LocExtension()
        {
        }

        public LocExtension(string resourceKey)
        {
            _resourceKey = resourceKey;
        }

        /// <summary>
        /// Gets or sets the key to use when finding a resource in the active localization ResourceDictionary. 
        ///
        /// Usage:
        ///
        ///    <Grid
        ///        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        ///        xmlns:noesis="clr-namespace:NoesisGUIExtensions"
        ///        noesis:Loc.Source="Locale_fr.xaml">
        ///        <TextBlock Text="{noesis:Loc Title}"/>
        ///        <Image Source="{noesis:Loc IntroBackground}"/>
        ///    </Grid>
        ///
        /// </summary>
        public string ResourceKey
        {
            get { return _resourceKey; }
            set { _resourceKey = value; }
        }

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            IProvideValueTarget valueTarget = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
            if (valueTarget == null || !(valueTarget.TargetObject is DependencyObject target))
            {
                return this;
            }

            DependencyProperty targetProperty = (DependencyProperty)valueTarget.TargetProperty;

            LocMonitor monitor = (LocMonitor)target.GetValue(MonitorProperty);

            if (monitor == null)
            {
                monitor = new LocMonitor(target);
                target.SetValue(MonitorProperty, monitor);
            }

            return monitor.AddDependencyProperty(targetProperty, _resourceKey);
        }

        #region Source attached property

        public static readonly DependencyProperty SourceProperty =
            DependencyProperty.RegisterAttached(
                "Source",
                typeof(Uri),
                typeof(LocExtension),
                new PropertyMetadata(null, SourceChangedCallback)
            );

        private static void SourceChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Uri source = (Uri)d.GetValue(SourceProperty);

            if (source == null)
            {
                d.SetValue(ResourcesProperty, null);
                return;
            }

            ResourceDictionary resourceDictionary = new ResourceDictionary
            {
                Source = source
            };

            d.SetValue(ResourcesProperty, resourceDictionary);
        }

        public static Uri GetSource(UIElement target)
        {
            return (Uri)target.GetValue(SourceProperty);
        }

        public static void SetSource(UIElement target, Uri value)
        {
            target.SetValue(SourceProperty, value);
        }

        #endregion

        #region Resources attached property

        public static readonly DependencyProperty ResourcesProperty =
            DependencyProperty.RegisterAttached(
                "Resources",
                typeof(ResourceDictionary),
                typeof(LocExtension),
                new FrameworkPropertyMetadata(null, flags: FrameworkPropertyMetadataOptions.Inherits, ResourcesChangedCallback)
            );

        private static void ResourcesChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            LocMonitor monitor = (LocMonitor)d.GetValue(MonitorProperty);

            if (monitor != null)
            {
                if (monitor.TargetObject != d)
                {
                    monitor = monitor.Clone(d);
                    d.SetValue(MonitorProperty, monitor);
                }
                monitor.InvalidateResources((ResourceDictionary)e.NewValue);
            }
        }

        public static ResourceDictionary GetResources(DependencyObject dependencyObject)
        {
            return (ResourceDictionary)dependencyObject.GetValue(ResourcesProperty);
        }

        public static void SetResources(DependencyObject dependencyObject, ResourceDictionary resources)
        {
            dependencyObject.SetValue(ResourcesProperty, resources);
        }

        #endregion

        #region Monitor attached property

        private static readonly DependencyProperty MonitorProperty =
            DependencyProperty.RegisterAttached(
                ".Monitor",
                typeof(LocMonitor),
                typeof(LocExtension),
                new PropertyMetadata(null)
            );

        #endregion
    }

    internal class LocMonitor
    {
        private readonly List<Tuple<DependencyProperty, string>> _monitoredDependencyProperties =
            new List<Tuple<DependencyProperty, string>>();

        public DependencyObject TargetObject { get; }

        public LocMonitor(DependencyObject targetObject)
        {
            TargetObject = targetObject;
        }

        public object AddDependencyProperty(DependencyProperty targetProperty, string resourceKey)
        {
            ResourceDictionary resourceDictionary = LocExtension.GetResources(TargetObject);

            for (int i = 0; i < _monitoredDependencyProperties.Count; i++)
            {
                if (_monitoredDependencyProperties[i].Item1 == targetProperty)
                {
                    _monitoredDependencyProperties[i] =
                        new Tuple<DependencyProperty, string>(_monitoredDependencyProperties[i].Item1, resourceKey);

                    return Evaluate(targetProperty, resourceKey, resourceDictionary);
                }
            }

            _monitoredDependencyProperties.Add(new Tuple<DependencyProperty, string>(targetProperty, resourceKey));

            return Evaluate(targetProperty, resourceKey, resourceDictionary);
        }

        public void InvalidateResources(ResourceDictionary resourceDictionary)
        {
            foreach (Tuple<DependencyProperty, string> entry in _monitoredDependencyProperties)
            {
                TargetObject.SetValue(entry.Item1,
                    Evaluate(entry.Item1, entry.Item2, resourceDictionary));
            }
        }

        public LocMonitor Clone(DependencyObject targetObject)
        {
            LocMonitor clone = new LocMonitor(targetObject);
            clone._monitoredDependencyProperties.AddRange(_monitoredDependencyProperties);
            return clone;
        }

        private static object Evaluate(DependencyProperty targetProperty, string resourceKey,
            ResourceDictionary resourceDictionary)
        {
            if (resourceDictionary != null && resourceDictionary.Contains(resourceKey))
            {
                object resource = resourceDictionary[resourceKey];
                if (resource != null)
                {
                    return resource;
                }

                Console.WriteLine($"[NOESIS/E] Resource key '{resourceKey}' not found in Loc Resources");
            }

            if (targetProperty.PropertyType == typeof(string))
            {
                return $"<Loc !{resourceKey}>";
            }

            return null;
        }
    }
}
LocExtension.cs (9,673 bytes)   
maherne

maherne

2024-02-28 12:05

developer   ~0009251

I discovered that Blend can pass C# Reflection data to ProvideValue during it's IntelliSense process, this was leading to a "Unable to cast object of type 'System.Reflection.RuntimePropertyInfo' to type 'System.Windows.DependencyProperty'" error. This error does not affect anything during design or runtime.

I have attached an updated version of WPF LocExtension.cs, which fixes this error from occurring, and this fix will be included in our Nuget package update.

LocExtension-2.cs (9,680 bytes)   
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Markup;

namespace NoesisGUIExtensions
{
    /// <summary>
    /// Implements a markup extension that supports references to a localization ResourceDictionary.
    ///
    /// Provides a value for any XAML property attribute by looking up a reference in the
    /// ResourceDictionary defined by the Source attached property. Values will be re-evaluated when
    /// the Source attached property changes.
    ///
    /// If used with a string or object property, and the provided resource key is not found, the
    /// LocExtension will return a string in the format "<Loc !%s>" where %s is replaced with the key.
    ///
    /// This example shows the full setup for a LocExtension. It utilizes the RichText attached
    /// property to support BBCode markup in the localized strings. The Loc.Source property references
    /// the "Language_en-gb.xaml" ResourceDictionary below.
    ///
    /// Usage:
    ///
    ///    <StackPanel
    ///      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    ///      xmlns:noesis="clr-namespace:NoesisGUIExtensions"
    ///      noesis:Loc.Source="Language_en-gb.xaml">
    ///      <Image Source="{noesis:Loc Flag}"/>
    ///      <TextBlock noesis:RichText.Text="{noesis:Loc TitleLabel}"/>
    ///      <TextBlock noesis:RichText.Text="{noesis:Loc SoundLabel}"/>
    ///    </StackPanel>
    /// 
    /// This is the contents of a "Language_en-gb.xaml" localized ResourceDictionary:
    ///
    /// Usage:
    ///
    ///    <ResourceDictionary
    ///      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    ///      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    ///      xmlns:sys="clr-namespace:System;assembly=mscorlib">
    ///      <ImageBrush x:Key="Flag" ImageSource="Flag_en-gb.png" Stretch="Fill"/>
    ///      <sys:String x:Key="TitleLabel">[b]LOCALIZATION SAMPLE[/b]</sys:String>
    ///      <sys:String x:Key="SoundLabel">A [i]sound[/i] label</sys:String>
    ///    </ResourceDictionary>
    ///
    /// </summary>
    [ContentProperty("ResourceKey")]
    public class LocExtension : MarkupExtension
    {
        private string _resourceKey;

        public LocExtension()
        {
        }

        public LocExtension(string resourceKey)
        {
            _resourceKey = resourceKey;
        }

        /// <summary>
        /// Gets or sets the key to use when finding a resource in the active localization ResourceDictionary. 
        ///
        /// Usage:
        ///
        ///    <Grid
        ///        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        ///        xmlns:noesis="clr-namespace:NoesisGUIExtensions"
        ///        noesis:Loc.Source="Locale_fr.xaml">
        ///        <TextBlock Text="{noesis:Loc Title}"/>
        ///        <Image Source="{noesis:Loc IntroBackground}"/>
        ///    </Grid>
        ///
        /// </summary>
        public string ResourceKey
        {
            get { return _resourceKey; }
            set { _resourceKey = value; }
        }

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            IProvideValueTarget valueTarget = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
            if (valueTarget == null 
                || !(valueTarget.TargetObject is DependencyObject target) 
                || !(valueTarget.TargetProperty is DependencyProperty targetProperty))
            {
                return this;
            }

            LocMonitor monitor = (LocMonitor)target.GetValue(MonitorProperty);

            if (monitor == null)
            {
                monitor = new LocMonitor(target);
                target.SetValue(MonitorProperty, monitor);
            }

            return monitor.AddDependencyProperty(targetProperty, _resourceKey);
        }

        #region Source attached property

        public static readonly DependencyProperty SourceProperty =
            DependencyProperty.RegisterAttached(
                "Source",
                typeof(Uri),
                typeof(LocExtension),
                new PropertyMetadata(null, SourceChangedCallback)
            );

        private static void SourceChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Uri source = (Uri)d.GetValue(SourceProperty);

            if (source == null)
            {
                d.SetValue(ResourcesProperty, null);
                return;
            }

            ResourceDictionary resourceDictionary = new ResourceDictionary
            {
                Source = source
            };

            d.SetValue(ResourcesProperty, resourceDictionary);
        }

        public static Uri GetSource(UIElement target)
        {
            return (Uri)target.GetValue(SourceProperty);
        }

        public static void SetSource(UIElement target, Uri value)
        {
            target.SetValue(SourceProperty, value);
        }

        #endregion

        #region Resources attached property

        public static readonly DependencyProperty ResourcesProperty =
            DependencyProperty.RegisterAttached(
                "Resources",
                typeof(ResourceDictionary),
                typeof(LocExtension),
                new FrameworkPropertyMetadata(null, flags: FrameworkPropertyMetadataOptions.Inherits, ResourcesChangedCallback)
            );

        private static void ResourcesChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            LocMonitor monitor = (LocMonitor)d.GetValue(MonitorProperty);

            if (monitor != null)
            {
                if (monitor.TargetObject != d)
                {
                    monitor = monitor.Clone(d);
                    d.SetValue(MonitorProperty, monitor);
                }
                monitor.InvalidateResources((ResourceDictionary)e.NewValue);
            }
        }

        public static ResourceDictionary GetResources(DependencyObject dependencyObject)
        {
            return (ResourceDictionary)dependencyObject.GetValue(ResourcesProperty);
        }

        public static void SetResources(DependencyObject dependencyObject, ResourceDictionary resources)
        {
            dependencyObject.SetValue(ResourcesProperty, resources);
        }

        #endregion

        #region Monitor attached property

        private static readonly DependencyProperty MonitorProperty =
            DependencyProperty.RegisterAttached(
                ".Monitor",
                typeof(LocMonitor),
                typeof(LocExtension),
                new PropertyMetadata(null)
            );

        #endregion
    }

    internal class LocMonitor
    {
        private readonly List<Tuple<DependencyProperty, string>> _monitoredDependencyProperties =
            new List<Tuple<DependencyProperty, string>>();

        public DependencyObject TargetObject { get; }

        public LocMonitor(DependencyObject targetObject)
        {
            TargetObject = targetObject;
        }

        public object AddDependencyProperty(DependencyProperty targetProperty, string resourceKey)
        {
            ResourceDictionary resourceDictionary = LocExtension.GetResources(TargetObject);

            for (int i = 0; i < _monitoredDependencyProperties.Count; i++)
            {
                if (_monitoredDependencyProperties[i].Item1 == targetProperty)
                {
                    _monitoredDependencyProperties[i] =
                        new Tuple<DependencyProperty, string>(_monitoredDependencyProperties[i].Item1, resourceKey);

                    return Evaluate(targetProperty, resourceKey, resourceDictionary);
                }
            }

            _monitoredDependencyProperties.Add(new Tuple<DependencyProperty, string>(targetProperty, resourceKey));

            return Evaluate(targetProperty, resourceKey, resourceDictionary);
        }

        public void InvalidateResources(ResourceDictionary resourceDictionary)
        {
            foreach (Tuple<DependencyProperty, string> entry in _monitoredDependencyProperties)
            {
                TargetObject.SetValue(entry.Item1,
                    Evaluate(entry.Item1, entry.Item2, resourceDictionary));
            }
        }

        public LocMonitor Clone(DependencyObject targetObject)
        {
            LocMonitor clone = new LocMonitor(targetObject);
            clone._monitoredDependencyProperties.AddRange(_monitoredDependencyProperties);
            return clone;
        }

        private static object Evaluate(DependencyProperty targetProperty, string resourceKey,
            ResourceDictionary resourceDictionary)
        {
            if (resourceDictionary != null && resourceDictionary.Contains(resourceKey))
            {
                object resource = resourceDictionary[resourceKey];
                if (resource != null)
                {
                    return resource;
                }

                Console.WriteLine($"[NOESIS/E] Resource key '{resourceKey}' not found in Loc Resources");
            }

            if (targetProperty.PropertyType == typeof(string))
            {
                return $"<Loc !{resourceKey}>";
            }

            return null;
        }
    }
}
LocExtension-2.cs (9,680 bytes)   
sfernandez

sfernandez

2024-02-28 12:25

manager   ~0009252

We just released Noesis.GUI.Extensions 3.0.27 to fix both issues.

samc

samc

2024-02-28 18:52

reporter   ~0009258

I have the latest version of the plugin, and now I'm seeing a new exception in the same spot:

Severity Code Description Project File Line Suppression State Details
Error XDG0010 Object of type 'NoesisGUIExtensions.LocExtension' cannot be converted to type 'System.String'. HUD D:\projects\torch\Assets\Scripts\Runtime\Client\UI\FrontEnd\FrontEndView.xaml 288

I'll try to get some more information about this

samc

samc

2024-02-28 19:04

reporter   ~0009259

Adding some screen shots of teh exception - one i caught in the debugger and the other of the way the exception shows up in the designer.

sfernandez

sfernandez

2024-02-28 19:42

manager   ~0009261

One question, is the SessionLabelText a CLR or a DP property?

sfernandez

sfernandez

2024-02-28 20:59

manager   ~0009262

Ok, we've been able to reproduce it. I generated a new version 3.0.28 of the Nuget for you to try.
Hopefully we got everything right now :)

samc

samc

2024-02-28 21:08

reporter   ~0009263

Looks good! feel free to close the bug now

Issue History

Date Modified Username Field Change
2024-02-27 18:27 samc New Issue
2024-02-27 18:27 samc Tag Attached: C#
2024-02-27 18:49 maherne Note Added: 0009245
2024-02-27 18:49 maherne File Added: LocExtension.cs
2024-02-27 18:50 maherne Assigned To => maherne
2024-02-27 18:50 maherne Status new => assigned
2024-02-27 18:50 maherne Target Version => 3.2.4
2024-02-27 18:51 maherne Status assigned => resolved
2024-02-27 18:51 maherne Resolution open => fixed
2024-02-27 18:51 maherne Fixed in Version => 3.2.4
2024-02-27 18:52 maherne Status resolved => feedback
2024-02-27 18:52 maherne Resolution fixed => reopened
2024-02-28 12:05 maherne Note Added: 0009251
2024-02-28 12:05 maherne File Added: LocExtension-2.cs
2024-02-28 12:25 sfernandez Status feedback => resolved
2024-02-28 12:25 sfernandez Resolution reopened => fixed
2024-02-28 12:25 sfernandez Fixed in Version 3.2.4 =>
2024-02-28 12:25 sfernandez Note Added: 0009252
2024-02-28 18:52 samc Status resolved => feedback
2024-02-28 18:52 samc Resolution fixed => reopened
2024-02-28 18:52 samc Note Added: 0009258
2024-02-28 19:04 samc Note Added: 0009259
2024-02-28 19:04 samc File Added: Screenshot 2024-02-28 100052.png
2024-02-28 19:04 samc File Added: Screenshot 2024-02-28 100354.png
2024-02-28 19:04 samc Status feedback => assigned
2024-02-28 19:42 sfernandez Status assigned => feedback
2024-02-28 19:42 sfernandez Note Added: 0009261
2024-02-28 20:59 sfernandez Note Added: 0009262
2024-02-28 21:08 samc Note Added: 0009263
2024-02-28 21:08 samc Status feedback => assigned
2024-02-29 10:37 sfernandez Status assigned => resolved
2024-02-29 10:37 sfernandez Resolution reopened => fixed