String format with a markup extension

隐身守侯 提交于 2019-11-30 17:47:53

See if the following works for you. I took the test case you offered in the comment and expanded it slightly to better illustrate the mechanism. I guess the key is to keep flexibility by using DependencyProperties in the nesting container.

EDIT: I have replaced the blend behavior with a subclass of the TextBlock. This adds easier linkage for DataContext and DynamicResources.

On a sidenote, the way your project uses DynamicResources to introduce conditions is not something I would recommend. Instead try using the ViewModel to establish the conditions, and/or use Triggers.

Xaml:

<UserControl x:Class="WpfApplication1.Controls.ExpiryView" xmlns:system="clr-namespace:System;assembly=mscorlib" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
                 xmlns:props="clr-namespace:WpfApplication1.Properties" xmlns:models="clr-namespace:WpfApplication1.Models"
                 xmlns:h="clr-namespace:WpfApplication1.Helpers" xmlns:c="clr-namespace:WpfApplication1.CustomControls"
                 Background="#FCF197" FontFamily="Segoe UI"
                 TextOptions.TextFormattingMode="Display">    <!-- please notice the effect of this on font fuzzyness -->

    <UserControl.DataContext>
        <models:ExpiryViewModel />
    </UserControl.DataContext>
    <UserControl.Resources>
        <system:String x:Key="ShortOrLongDateFormat">{0:d}</system:String>
    </UserControl.Resources>
    <Grid>
        <StackPanel>
            <c:TextBlockComplex VerticalAlignment="Center" HorizontalAlignment="Center">
                <c:TextBlockComplex.Content>
                    <h:StringFormatContainer StringFormat="{x:Static props:Resources.ExpiryDate}">
                        <h:StringFormatContainer.Values>
                            <h:StringFormatContainer Value="{Binding ExpiryDate}" StringFormat="{DynamicResource ShortOrLongDateFormat}" />
                            <h:StringFormatContainer Value="{Binding SecondsToExpiry}" />
                        </h:StringFormatContainer.Values>
                    </h:StringFormatContainer>
                </c:TextBlockComplex.Content>
            </c:TextBlockComplex>
        </StackPanel>
    </Grid>
</UserControl>

TextBlockComplex:

using System;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using WpfApplication1.Helpers;

namespace WpfApplication1.CustomControls
{
    public class TextBlockComplex : TextBlock
    {
        // Content
        public StringFormatContainer Content { get { return (StringFormatContainer)GetValue(ContentProperty); } set { SetValue(ContentProperty, value); } }
        public static readonly DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof(StringFormatContainer), typeof(TextBlockComplex), new PropertyMetadata(null));

        private static readonly DependencyPropertyDescriptor _dpdValue = DependencyPropertyDescriptor.FromProperty(StringFormatContainer.ValueProperty, typeof(StringFormatContainer));
        private static readonly DependencyPropertyDescriptor _dpdValues = DependencyPropertyDescriptor.FromProperty(StringFormatContainer.ValuesProperty, typeof(StringFormatContainer));
        private static readonly DependencyPropertyDescriptor _dpdStringFormat = DependencyPropertyDescriptor.FromProperty(StringFormatContainer.StringFormatProperty, typeof(StringFormatContainer));
        private static readonly DependencyPropertyDescriptor _dpdContent = DependencyPropertyDescriptor.FromProperty(TextBlockComplex.ContentProperty, typeof(StringFormatContainer));

        private EventHandler _valueChangedHandler;
        private NotifyCollectionChangedEventHandler _valuesChangedHandler;

        protected override IEnumerator LogicalChildren { get { yield return Content; } }

        static TextBlockComplex()
        {
            // take default style from TextBlock
            DefaultStyleKeyProperty.OverrideMetadata(typeof(TextBlockComplex), new FrameworkPropertyMetadata(typeof(TextBlock)));
        }

        public TextBlockComplex()
        {
            _valueChangedHandler = delegate { AddListeners(this.Content); UpdateText(); };
            _valuesChangedHandler = delegate { AddListeners(this.Content); UpdateText(); };

            this.Loaded += TextBlockComplex_Loaded;
        }

        void TextBlockComplex_Loaded(object sender, RoutedEventArgs e)
        {
            OnContentChanged(this, EventArgs.Empty); // initial call

            _dpdContent.AddValueChanged(this, _valueChangedHandler);
            this.Unloaded += delegate { _dpdContent.RemoveValueChanged(this, _valueChangedHandler); };
        }

        /// <summary>
        /// Reacts to a new topmost StringFormatContainer
        /// </summary>
        private void OnContentChanged(object sender, EventArgs e)
        {
            this.AddLogicalChild(this.Content); // inherits DataContext
            _valueChangedHandler(this, EventArgs.Empty);
        }

        /// <summary>
        /// Updates Text to the Content values
        /// </summary>
        private void UpdateText()
        {
            this.Text = Content.GetValue() as string;
        }

        /// <summary>
        /// Attaches listeners for changes in the Content tree
        /// </summary>
        private void AddListeners(StringFormatContainer cont)
        {
            // in case they have been added before
            RemoveListeners(cont);

            // listen for changes to values collection
            cont.CollectionChanged += _valuesChangedHandler;

            // listen for changes in the bindings of the StringFormatContainer
            _dpdValue.AddValueChanged(cont, _valueChangedHandler);
            _dpdValues.AddValueChanged(cont, _valueChangedHandler);
            _dpdStringFormat.AddValueChanged(cont, _valueChangedHandler);

            // prevent memory leaks
            cont.Unloaded += delegate { RemoveListeners(cont); };

            foreach (var c in cont.Values) AddListeners(c); // recursive
        }

        /// <summary>
        /// Detaches listeners
        /// </summary>
        private void RemoveListeners(StringFormatContainer cont)
        {
            cont.CollectionChanged -= _valuesChangedHandler;

            _dpdValue.RemoveValueChanged(cont, _valueChangedHandler);
            _dpdValues.RemoveValueChanged(cont, _valueChangedHandler);
            _dpdStringFormat.RemoveValueChanged(cont, _valueChangedHandler);
        }
    }
}

StringFormatContainer:

using System.Linq;
using System.Collections;
using System.Collections.ObjectModel;
using System.Windows;

namespace WpfApplication1.Helpers
{
    public class StringFormatContainer : FrameworkElement
    {
        // Values
        private static readonly DependencyPropertyKey ValuesPropertyKey = DependencyProperty.RegisterReadOnly("Values", typeof(ObservableCollection<StringFormatContainer>), typeof(StringFormatContainer), new FrameworkPropertyMetadata(new ObservableCollection<StringFormatContainer>()));
        public static readonly DependencyProperty ValuesProperty = ValuesPropertyKey.DependencyProperty;
        public ObservableCollection<StringFormatContainer> Values { get { return (ObservableCollection<StringFormatContainer>)GetValue(ValuesProperty); } }

        // StringFormat
        public static readonly DependencyProperty StringFormatProperty = DependencyProperty.Register("StringFormat", typeof(string), typeof(StringFormatContainer), new PropertyMetadata(default(string)));
        public string StringFormat { get { return (string)GetValue(StringFormatProperty); } set { SetValue(StringFormatProperty, value); } }

        // Value
        public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(object), typeof(StringFormatContainer), new PropertyMetadata(default(object)));
        public object Value { get { return (object)GetValue(ValueProperty); } set { SetValue(ValueProperty, value); } }

        public StringFormatContainer()
            : base()
        {
            SetValue(ValuesPropertyKey, new ObservableCollection<StringFormatContainer>());
            this.Values.CollectionChanged += OnValuesChanged;
        }

        /// <summary>
        /// The implementation of LogicalChildren allows for DataContext propagation.
        /// This way, the DataContext needs only be set on the outermost instance of StringFormatContainer.
        /// </summary>
        void OnValuesChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if (e.NewItems != null)
            {
                foreach (var value in e.NewItems)
                    AddLogicalChild(value);
            }
            if (e.OldItems != null)
            {
                foreach (var value in e.OldItems)
                    RemoveLogicalChild(value);
            }
        }

        /// <summary>
        /// Recursive function to piece together the value from the StringFormatContainer hierarchy
        /// </summary>
        public object GetValue()
        {
            object value = null;
            if (this.StringFormat != null)
            {
                // convention: if StringFormat is set, Values take precedence over Value
                if (this.Values.Any())
                    value = string.Format(this.StringFormat, this.Values.Select(v => (object)v.GetValue()).ToArray());
                else if (Value != null)
                    value = string.Format(this.StringFormat, Value);
            }
            else
            {
                // convention: if StringFormat is not set, Value takes precedence over Values
                if (Value != null)
                    value = Value;
                else if (this.Values.Any())
                    value = string.Join(string.Empty, this.Values);
            }
            return value;
        }

        protected override IEnumerator LogicalChildren
        {
            get
            {
                if (Values == null) yield break;
                foreach (var v in Values) yield return v;
            }
        }
    }
}

ExpiryViewModel:

using System;
using System.ComponentModel;

namespace WpfApplication1.Models
{
    public class ExpiryViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        private DateTime _expiryDate;
        public DateTime ExpiryDate { get { return _expiryDate; } set { _expiryDate = value; OnPropertyChanged("ExpiryDate"); } }

        public int SecondsToExpiry { get { return (int)ExpiryDate.Subtract(DateTime.Now).TotalSeconds; } }

        public ExpiryViewModel()
        {
            this.ExpiryDate = DateTime.Today.AddDays(2.67);

            var timer = new System.Timers.Timer(1000);
            timer.Elapsed += (s, e) => OnPropertyChanged("SecondsToExpiry");
            timer.Start();
        }
    }
}

you can combine the use of Binding with Resources as well as Properties :

Sample :

XAML :

   <Window x:Class="Stackoverflow.MainWindow"
        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"  
        xmlns:local="clr-namespace:Stackoverflow"    
        Title="MainWindow" Height="350" Width="525">
         <Window.Resources>
           <local:StringFormatConverter x:Key="stringFormatConverter" />
           <sys:String x:Key="textResource">Kill me</sys:String>
         </Window.Resources>

         <Grid>
             <TextBlock>
                 <TextBlock.Text>
                     <MultiBinding Converter="{StaticResource stringFormatConverter}">
                          <Binding Path="SomeText" />
                          <Binding Source="{StaticResource textResource}" />                   
                      </MultiBinding>
                 </TextBlock.Text>
              </TextBlock>
          </Grid>
   </Window>

CS :

     public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = this;
    }

    public string SomeText
    {
        get { return "Please"; }
    }

}

public class StringFormatConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return string.Format("{0} {1}", (string)values[0], (string)values[1]);
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Edit :

here's a work around for now

   <Window.Resources>
       <local:StringFormatConverter x:Key="stringFormatConverter" />
       <sys:String x:Key="textResource">Kill me</sys:String>
   </Window.Resources>

     <Grid>
         <TextBlock Tag="{DynamicResource textResource}">
             <TextBlock.Text>
                 <MultiBinding Converter="{StaticResource stringFormatConverter}">
                      <Binding Path="SomeText" />
                      <Binding Path="Tag" RelativeSource="{RelativeSource Self}" />                   
                  </MultiBinding>
             </TextBlock.Text>
          </TextBlock>
      </Grid>

i'll think of something else later .

I know I'm not exactly answering your question, but there is already a mechanism in wpf that allows for string formatting in xaml, it is BindingBase.StringFormat property

I haven't figured out how to make it work with DynamicResource binding, but it works with other bindings, such as binding to the property of data context, to static resource or to the property of another element.

     <TextBlock> 
        <TextBlock.Resources>
            <clr:String x:Key="ARG2ID">111</clr:String>
        </TextBlock.Resources>
    <TextBlock.Text> 
        <MultiBinding StringFormat="Name:{0}, Surname:{1} Age:{2}"> 
            <Binding Path="Name" />
            <Binding ElementName="txbSomeTextBox" Path="Text" Mode="OneWay" />
            <Binding Source="{StaticResource ARG2ID}" Mode="OneWay" />
        </MultiBinding> 
    </TextBlock.Text>
    </TextBlock>

If you really want to implement your own markup extension that takes a binding, there is a way. I implemented a markup extension that takes a name of a picture (or a binding to something that holds it) as a constructor argument, then resolves the path and returns the ImageSource.

I implemented it based on this artcle.

Since I'm bad at explaining, I better illustrate it using code:

<Image  Name="imgPicture"
             Source="{utils:ImgSource {Binding Path=DataHolder.PictureName}}" />
<Image  Name="imgPicture"
             Source="{utils:ImgSource C:\\SomeFolder\\picture1.png}" />
<Image  Name="imgPicture"
             Source="{utils:ImgSource SomePictureName_01}" />

the extension class:

    public class ImgSourceExtension : MarkupExtension
        {
            [ConstructorArgument("Path")] // IMPORTANT!!
            public object Path { get; set; }

            public ImgSourceExtension():base() { }

            public ImgSourceExtension(object Path)
                : base()
            {
                this.Path = Path;
            }

            public override object ProvideValue(IServiceProvider serviceProvider)
            {
                object returnValue = null;
                try
                {
                    IProvideValueTarget service = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));

                    Binding binding = null;

                    if (this.Path is string)
                    {
                        binding = new Binding { Mode = BindingMode.OneWay };
                    }
                    else if (this.Path is Binding)
                    {
                        binding = Path as Binding;
                    }
else  if (this.Path is ImageSource) return this.Path;
                else if (this.Path is System.Windows.Expression)
                {
                    ResourceReferenceExpressionConverter cnv = new ResourceReferenceExpressionConverter();
                    DynamicResourceExtension mex = null;
                    try
                    {
                        mex = (MarkupExtension)cnv.ConvertTo(this.Path, typeof(MarkupExtension))
                            as DynamicResourceExtension;
                    }
                    catch (Exception) { }

                    if (mex != null)
                    {
                        FrameworkElement targetObject = service.TargetObject as FrameworkElement;
                        if (targetObject == null)
                        {
                            return Utils.GetEmpty(); 
                        }
                        return targetObject.TryFindResource(mex.ResourceKey as string);
                    }
                }
                    else return Utils.GetEmpty();


                    binding.Converter = new Converter_StringToImageSource();
                    binding.ConverterParameter = Path is Binding ? null : Path as string;

                    returnValue = binding.ProvideValue(serviceProvider);
                }
                catch (Exception) { returnValue = Utils.GetEmpty(); }
                return returnValue;
            }
        }

The converter:

[ValueConversion(typeof(string), typeof(ImageSource))]
    class Converter_StringToImageSource : MarkupExtension, IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            try
            {
                var key = (value as string ?? parameter as string);

                if (!string.IsNullOrEmpty(key))
                {
                    // Do translation based on the key
                    if (File.Exists(key))
                    {
                        var source = new BitmapImage(new Uri(key));
                        return source;
                    }
                    else
                    {
                        var source = new BitmapImage(new Uri(Utils.GetPicturePath(key)));
                        return source;
                    }

                }
                return Utils.GetEmpty();
            }
            catch (Exception)
            {
                return Utils.GetEmpty();
            }
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }

        public Converter_StringToImageSource()
            : base()
        {
        }

        private static Converter_StringToImageSource _converter = null;

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            if (_converter == null) _converter = new Converter_StringToImageSource();
            return _converter;
        }
    }

EDIT:

I updated the ImgSourceExtension so now it will work with StaticResource and DynamicResource, although I still don't know how to do the sort of nested binding the OP is looking for.

Having said that, during my research yesterday I stumbled upon an interesting "hack" related to binding to dynamic resources. I think combining it with a SortedList or another collection data type that can be accessed by key may be worth looking into:

 xmlns:col="clr-namespace:System.Collections;assembly=mscorlib"
 xmlns:sys="clr-namespace:System;assembly=mscorlib"
...
<Window.Resources>
        <col:SortedList x:Key="stringlist">
            <sys:String x:Key="key0">AAA</sys:String>
            <sys:String x:Key="key1">BBB</sys:String>
            <sys:String x:Key="key2">111</sys:String>
            <sys:String x:Key="key3">some text</sys:String>
        </col:SortedList>
    </Window.Resources>
....
   <TextBlock Name="txbTmp" DataContext="{DynamicResource stringlist}"> 
        <TextBlock.Text> 
            <MultiBinding StringFormat="Name:{0}, Surname:{1} Age:{2}"> 
                <Binding Path="[key0]" />
                <Binding Path="[key1]"/>
                <Binding Path="[key2]" />
            </MultiBinding> 
        </TextBlock.Text>
    </TextBlock>

The only drawback I encountered is that, when changing the values in the stringlist, the resource has to be reassigned:

  SortedList newresource = new SortedList(((SortedList)Resources["stringlist"]));
  newresource["key0"] = "1234";
  this.Resources["stringlist"] = newresource;

I think I just solved the old problem of switching culture at runtime quite neatly.

The way I see it, there are two possibilities:

  1. We accept that you will need DynamicResources for your localization and write a markup extension, which is pretty much what you have tried and seems hard to achieve.
  2. We just use StaticResources, in which case the world of bindings becomes much easier, yet updating already bound strings becomes trickier.

I suggest the latter. Basically my idea is to use a proxy to the resx file which is able to update all bindings once the culture changes. This article by OlliFromTor went a long way towards providing the implementation.

For deeper nesting, there's the limitation that StringFormat does not accept bindings, so you might still have to introduce a converter if the StringFormats cannot be kept static.

Resx structure:

Resx contents (default/no/es):

Xaml:

<UserControl x:Class="WpfApplication1.Controls.LoginView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:props="clr-namespace:WpfApplication1.Properties"
             xmlns:models="clr-namespace:WpfApplication1.Models"
             Background="#FCF197" 
             FontFamily="Segoe UI"
             TextOptions.TextFormattingMode="Display"> <!-- please notice the effect of this on font fuzzyness -->

    <UserControl.DataContext>
        <models:LoginViewModel />
    </UserControl.DataContext>
    <UserControl.Resources>
        <Thickness x:Key="StdMargin">5,2</Thickness>
        <Style TargetType="{x:Type TextBlock}">
            <Setter Property="Margin" Value="{StaticResource StdMargin}"/>
            <Setter Property="VerticalAlignment" Value="Center"/>
        </Style>
        <Style TargetType="{x:Type Button}">
            <Setter Property="Margin" Value="{StaticResource StdMargin}"/>
            <Setter Property="MinWidth" Value="80"/>
        </Style>
        <Style TargetType="{x:Type TextBox}">
            <Setter Property="Margin" Value="{StaticResource StdMargin}"/>
        </Style>
        <Style TargetType="{x:Type ComboBox}">
            <Setter Property="Margin" Value="{StaticResource StdMargin}"/>
        </Style>

    </UserControl.Resources>

    <Grid Margin="30" Height="150" Width="200">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*" MinWidth="120"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding Username, Source={StaticResource Resx}}" />
        <TextBlock Grid.Row="1" Grid.Column="0" Text="{Binding Password, Source={StaticResource Resx}}" />
        <TextBlock Grid.Row="2" Grid.Column="0" Text="{Binding Language, Source={StaticResource Resx}}" />
        <TextBox Grid.Row="0" Grid.Column="1" x:Name="tbxUsername" Text="{Binding Username, UpdateSourceTrigger=PropertyChanged}" />
        <TextBox Grid.Row="1" Grid.Column="1" x:Name="tbxPassword" Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}" />
        <ComboBox Grid.Row="2" Grid.Column="1" ItemsSource="{Binding Cultures}" DisplayMemberPath="DisplayName" SelectedItem="{Binding SelectedCulture}" />
        <TextBlock Grid.Row="3" Grid.ColumnSpan="2" Foreground="Blue" TextWrapping="Wrap" Margin="5,15,5,2">
            <TextBlock.Text>
                <MultiBinding StringFormat="{x:Static props:Resources.LoginMessage}">
                    <Binding Path="Username" />
                    <Binding Path="Password" />
                    <Binding Path="Language" Source="{StaticResource Resx}" />
                    <Binding Path="SelectedCulture.DisplayName" FallbackValue="(not set)" />
                </MultiBinding>
            </TextBlock.Text>
        </TextBlock>
    </Grid>
</UserControl>

I chose to add the instance of the ResourcesProxy to App.xaml, there are other possibilities (e.g. instantiating and exposing the proxy directly on the ViewModel)

<Application x:Class="WpfApplication1.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:props="clr-namespace:WpfApplication1.Properties"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <props:ResourcesProxy x:Key="Resx" />
    </Application.Resources>
</Application>

ViewModel:

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Threading;
using System.Windows;
using WpfApplication1.Properties;

namespace WpfApplication1.Models
{
    public class LoginViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));

            if (propertyName == "SelectedCulture")
                ChangeCulture();
        }

        private ObservableCollection<CultureInfo> _cultures;
        public ObservableCollection<CultureInfo> Cultures { get { return _cultures; } set { _cultures = value; OnPropertyChanged("Cultures"); } }

        private CultureInfo _selectedCulture;
        public CultureInfo SelectedCulture { get { return _selectedCulture; } set { _selectedCulture = value; OnPropertyChanged("SelectedCulture"); } }

        private string _username;
        public string Username { get { return _username; } set { _username = value; OnPropertyChanged("Username"); } }

        private string _password;
        public string Password { get { return _password; } set { _password = value; OnPropertyChanged("Password"); } }

        public LoginViewModel()
        {
            this.Cultures = new ObservableCollection<CultureInfo>()
            {
                new CultureInfo("no"),
                new CultureInfo("en"),
                new CultureInfo("es")
            };
        }

        private void ChangeCulture()
        {
            Thread.CurrentThread.CurrentCulture = this.SelectedCulture;
            Thread.CurrentThread.CurrentUICulture = this.SelectedCulture;

            var resx = Application.Current.Resources["Resx"] as ResourcesProxy;
            resx.ChangeCulture(this.SelectedCulture);
        }
    }
}

And finally the important part, the ResourcesProxy:

using System.ComponentModel;
using System.Dynamic;
using System.Globalization;
using System.Linq;
using System.Reflection;

namespace WpfApplication1.Properties
{
    /// <summary>
    /// Proxy to envelop a resx class and attach INotifyPropertyChanged behavior to it.
    /// Enables runtime change of language through the ChangeCulture method.
    /// </summary>
    public class ResourcesProxy : DynamicObject, INotifyPropertyChanged
    {
        private Resources _proxiedResources = new Resources(); // proxied resx

        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
                PropertyChanged(_proxiedResources, new PropertyChangedEventArgs(propertyName));
        }

        /// <summary>
        /// Sets the new culture on the resources and updates the UI
        /// </summary>
        public void ChangeCulture(CultureInfo newCulture)
        {
            Resources.Culture = newCulture;

            if (this.PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(null));
        }

        private PropertyInfo GetPropertyInfo(string propertyName)
        {
            return _proxiedResources.GetType().GetProperties().First(pi => pi.Name == propertyName);
        }

        private void SetMember(string propertyName, object value)
        {
            GetPropertyInfo(propertyName).SetValue(_proxiedResources, value, null);
            OnPropertyChanged(propertyName);
        }

        private object GetMember(string propertyName)
        {
            return GetPropertyInfo(propertyName).GetValue(_proxiedResources, null);
        }

        public override bool TryConvert(ConvertBinder binder, out object result)
        {
            if (binder.Type == typeof(INotifyPropertyChanged))
            {
                result = this;
                return true;
            }

            if (_proxiedResources != null && binder.Type.IsAssignableFrom(_proxiedResources.GetType()))
            {
                result = _proxiedResources;
                return true;
            }
            else
                return base.TryConvert(binder, out result);
        }

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            result = GetMember(binder.Name);
            return true;
        }

        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            SetMember(binder.Name, value);
            return true;
        }
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!