Bind a List<string> to a TextBox - c#

After 20+ years of windows programming and two days of WPF I feel I know nothing :-)
My first WPF program is really simple: You drop a few files from explorer and their names are shown in a TextBox control.
(It works fine for a ListBox, but that isn't what I want. And of course adding the lines manually in the Drop event works as well - but I want to learn about the Binding ways..)
So I wrote a Converter but somehow it insn't used (breakpoints won't get hit) and nothing shows up.
It should be a small thing or maybe I'm totally off track. Found many examples for similar things from which i patched this together but still can't get it to work.
(I probably won't need the ConvertBack, but wrote it down anyway..)
Here is the converter class:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
namespace WpTest02
{
public class ListToTextConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
StringBuilder sb = new StringBuilder();
foreach (string s in (List<string>)value) sb.AppendLine(s);
return sb.ToString();
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
string[] lines = ((string)value).Split(new string[] { #"\r\n" }, StringSplitOptions.RemoveEmptyEntries);
return lines.ToList<String>();
}
}
}
The MainWindow.xaml, where I suspect the binding problem to be:
<Window x:Class="WpTest02.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpTest02"
Title="MainWindow" Height="350" Width="525"
>
<Window.Resources>
<local:ListToTextConverter x:Key="converter1" />
</Window.Resources>
<Grid >
<TextBox Name="tb_files" Margin="50,20,0,0" AllowDrop="True"
PreviewDragOver="tb_files_PreviewDragOver" Drop="tb_files_Drop"
Text="{Binding Path=fileNames, Converter={StaticResource converter1} }"
/>
</Grid>
</Window>
And the Codebehind with nothing more the the data property to bind to and the drag&drop code, which works.
using System;
//etc ..
namespace WpTest02
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
fileNames = new List<string>();
}
public List<string> fileNames { get; set; }
private void tb_files_Drop(object sender, DragEventArgs e)
{
var files = ((DataObject)e.Data).GetFileDropList();
foreach (string s in files) fileNames.Add(s);
// EDIT: this doesn't help ? Wrong!
// EDIT: this is actually necessary! :
tb_files.GetBindingExpression(TextBox.TextProperty).UpdateTarget();
// this obviosly would work:
//foreach (string s in files) tb_files.Text += s + "\r\n";
}
private void tb_files_PreviewDragOver(object sender, DragEventArgs e)
{
e.Handled = true;
}
}
}
Note: I have editied the last piece of code to stress the importance of the UpdateTarget call.

For Binding to work you need to assign Window's DataContext to the instance where property resides which in your case is Window class itself.
So set DataContext in constructor and it should work fine:
public MainWindow()
{
InitializeComponent();
fileNames = new List<string>();
DataContext = this;
}
OR
You have to explicitly resolve the binding from XAML using ElementName in your binding:
<Window x:Name="myWindow">
....
<TextBox Text="{Binding Path=fileNames, ElementName=myWindow,
Converter={StaticResource converter1}}"/>
For XAML approach to work, you have to initialize the list before XAML gets loaded i.e. before InitializeComponent gets called.
fileNames = new List<string>();
InitializeComponent();

The DataContext of the TextBox must be set to bind the data. Like this:
public MainWindow()
{
InitializeComponent();
fileNames = new List<string>();
this.tb_files.DataContext = this;
}

this is the general pattern which should work for you. Feel free to contact me with any questions. Best of luck! ~Justin
<Window xmlns:vm="clr-namespace:YourProject.YourViewModelNamespace"
xmlns:vc="clr-namespace:YourProject.YourValueConverterNamespace">
<Window.Resources>
<vc:YourValueConverter x:key="YourValueConverter" />
</Window.Resources>
<Window.DataContext>
<vm:YourViewViewModel />
</Window.DataContext>
<TextBox Text="{Binding MyItems, Converter={StaticResource YourValueConverter}}"/>
</Window>
public class YourViewViewModel : ViewModelBase
{
ObservableCollection<string> _myItems;
ObservableCollection<string> MyItems
{
get { return _gameProfileListItems; }
set { _gameProfileListItems = value; OnPropertyChanged("MyItems"); }
}
public void SetMyItems()
{
// go and get your data here, transfer it to an observable collection
// and then assign it to this.GameProfileListItems (i would recommend writing a .ToObservableCollection() extension method for IEnumerable)
this.MyItems = SomeManagerOrUtil.GetYourData().ToObservableCollection();
}
}
public class YourView : Window
{
YourViewViewModel ViewModel
{
{ get return this.DataContext as YourViewViewModel; }
}
public void YourView()
{
InitializeComponent();
InitializeViewModel();
}
void InitializeViewModel()
{
this.ViewModel.SetMyItems();
}
}

Related

"Scrolling"-only Binding update of ItemsSource of ItemsControl. How to make refresh automatic? [duplicate]

I used to just create a block of text by converting a list of strings to one string with newlines. This Binding worked; updated when it was supposed to and all, but I'm trying to move the list of text into an ItemsControl as they will need to be hyperlinks at some point in the future. Problem: The ItemsControl does not change when the PropertyChangeEvent is fired. The Relevant Code is as follows:
Xaml
<local:BaseUserControl x:Class="BAC.Windows.UI.Views.ErrorsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:BAC.Windows.UI.Views"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
...
<ItemsControl ItemsSource="{Binding Path=ErrorMessages}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"></TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!--<TextBlock VerticalAlignment="Center" Visibility="{Binding ErrorMessages, Converter={StaticResource VisibleWhenNotEmptyConverter}}" Text="{Binding ErrorMessages, Converter={StaticResource ErrorMessagesToTextConverter}}">
(What I used to use)
</TextBlock>-->
...
</local:BaseUserControl>
ViewModel
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using ASI.Core.Core;
using ASI.Core.DTO;
using ASI.Core.Extensions;
using ASI.Core.Mappers;
using BAC.Core.Resources;
using BAC.Core.Services;
using BAC.Core.ViewModels.Views;
namespace BAC.Core.ViewModels
{
public interface IErrorsViewModel : IViewModel<IErrorsView>
{
}
public class ErrorsViewModel : BaseViewModel<IErrorsView>, IErrorsViewModel
{
...
private readonly ErrorDTO _errorDTO;
private readonly ErrorDTO _warningDTO;
public ErrorsViewModel(...) : base(view)
{
...
//Just added this string to know that it's at least binding. This Message displays, and never changes.
ErrorMessages = new List<string>() {"Simple Message"};
//Tells the View to bind dataContext to Viewmodel
Edit();
}
private void errorDTOOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
{
ErrorDTO dto;
if (!string.Equals(propertyChangedEventArgs.PropertyName, nameof(dto.HasError))) return;
ErrorMessages.Clear();
_errorDTO.ErrorMessages.Each(x => ErrorMessages.Add(Constants.Captions.Errors + ": " + x));
_warningDTO.ErrorMessages.Each(x => ErrorMessages.Add(Constants.Captions.Warnings + ": " + x));
OnPropertyChanged(() => ErrorMessages);
OnPropertyChanged(() => HasError);
OnPropertyChanged(() => HasWarning);
}
...
public bool HasError => _errorDTO.HasError;
public bool HasWarning => _warningDTO.HasError;
public IList<string> ErrorMessages { get; set; }
...
}
And just because I know people may ask to see it...
public class BaseNotifyPropertyChanged : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void OnPropertyChanged<T>(Expression<Func<T>> propertyExpression)
{
var body = propertyExpression.Body as MemberExpression;
if (body != null)
OnPropertyChanged(body.Member.Name);
}
protected void OnEvent(Action action)
{
try
{
action();
}
catch
{ }
}
}
I'm sure it's something stupidy simple I'm doing, but the harder I look, the more I get frusterated by what should something simple. Why does the binding work for all other conrols except ItemSource? What's so special about it?
I'll also add anotehr explanation (Even though I know this is old).
The reason this will not update the property is that the List object is not actually changing, so the ListView will not update the list. The only way to do this without using "ObservableCollection" is to create a brand new list on each property change like so:
private void errorDTOOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
{
if (!string.Equals(propertyChangedEventArgs.PropertyName, nameof(dto.HasError))) return;
OnPropertyChanged(() => ErrorMessages);
}
public List<string> ErrorMessages => getErrorMessages();
private List<string> getErrorMessages() {
//create list in a manner of your choosing
}
Hopefully that helps people when they run into this.
So I was able to get your code to work by using an ObservableCollection instead of the List. The ObservableCollection generates a list changed notification automatically when its collection is changed. Below is my sample code. I use a timer to update the error list every second.
<Window x:Class="TestEer.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TestEer"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525" DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
<ItemsControl ItemsSource="{Binding Path=ErrorMessages}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
using System.Collections.ObjectModel;
using System.Timers;
using System.Windows;
using System.Windows.Data;
namespace TestEer
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private Timer _timer;
private readonly object _sync = new object( );
public MainWindow( )
{
InitializeComponent( );
BindingOperations.EnableCollectionSynchronization( ErrorMessages, _sync );
_timer = new Timer
{
AutoReset = true,
Interval = 1000
};
_timer.Elapsed += _timer_Elapsed;
_timer.Enabled = true;
_timer.Start( );
}
private void _timer_Elapsed( object sender, ElapsedEventArgs e )
{
ErrorMessages.Add( $"Error # {e.SignalTime}" );
}
public ObservableCollection<string> ErrorMessages { get; } = new ObservableCollection<string>( );
}
}
We set up the OnPropertyChanged() method in the get set methods before the constructor and this seemed to work!
private bool _theString;
public bool TheString
{
get { return _theString; }
set { _theString = value; OnPropertyChanged(); }
}
Use {Binding TheString} in your .xaml.
Hope this helps!

WPF - Binding is working one time

I have the following begginer project, and have a wierd effect. I want the button to add one element to the ComboBox every time it is clicked.
What happens is that when I start the app, click n times on the button and then open the ComboBox, it shows me n items as expected. But no matter how often I click the button atfer that, there will be no new items in the ComboBox.
MainWindow.xaml:
<Window x:Class="WPF_binding_combobox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPF_binding_combobox"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<Button Content="Add item to combobox" Click="Button_Click" />
<ComboBox ItemsSource="{Binding Path=ComboBoxItems}" />
</StackPanel>
</Window>
MainWindow.xaml.cs (code-behind):
namespace WPF_binding_combobox {
using System.Windows;
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
this.DataContext = new La();
}
private void Button_Click(object sender, RoutedEventArgs e) {
La l = this.DataContext as La;
l.AddItem();
}
}
}
La.cs
namespace WPF_binding_combobox {
using System.Collections.Generic;
using System.ComponentModel;
class La : INotifyPropertyChanged {
private int counter;
private IList<string> cbItems;
public IList<string> ComboBoxItems {
get {
return this.cbItems;
}
set {
this.cbItems = value;
this.RaisePropertyChanged("ComboBoxItems");
}
}
public La() {
this.cbItems = new List<string>();
this.counter = 0;
}
public void AddItem() {
var temp = this.ComboBoxItems;
temp.Add("abc" + (++this.counter));
this.ComboBoxItems = temp;
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName) {
var handler = this.PropertyChanged;
if (handler != null) {
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
I have tried the debugger, and it shows me that there are items added to the backing field and the RaisePropertyChanged is called. For some reason, the UI does not show me any thing that changes after the first click.
I have tried setting Mode=OneWay or Mode=TowWay, there was no change.
Why is the ComboBox not getting updated after I open it for the first time?
You are using IList which has no way of informing View that items has been added or removed after initial binding (like you said).
You should use ObservableCollection. As the name suggests, it works on observer pattern where your View will be observer.
public ObservableCollection<string> ComboBoxItems
{
get
{
return cbItems;
}
}
and in AddItem you only need to to this
public void AddItem()
{
ComboBoxItems.Add("abc" + (++this.counter));
}
Off Topic:
If you want to use MVVM pattern, don't write any code in Code-behind. Use Delegate Commands to bind to buttons. (Google will help).
Even assigning DataContext can be removed to XAML like this
<Window.DataContext>
<myassembly:LA />
</Window.DataContext>
Till then, have a local variable in View which would save you casting hackles everytime.
private La _dataContext;
public MainWindow()
{
InitializeComponent();
_dataContext = new La();
this.DataContext = _dataContext
}
private void Button_Click(object sender, RoutedEventArgs e)
{
_dataContext.AddItem();
}
Whether you are using an ObservableCollection<T> or an IList<T> doesn't really matter if you reset the source collection property each time you want to update the ComboBox.
You should either use a single instance of an ObservableCollection that you add all items to or set your source property to a new IList<string>.
If you modify your AddItem() method slightly it works as expected:
public void AddItem()
{
var temp = new List<string>(this.ComboBoxItems);
temp.Add("abc" + (++this.counter));
this.ComboBoxItems = temp;
}

Do something after the usercontrol is loaded and displayed WPF

I have a UserControl and I am loading it dynamically in a ContentControl on a Button click. I have a TextBlock in the UserControl and I want to show some text dynamically (which is basically a status which my another method will return while processing a request) in the TextBlock after the UserControl is loaded. I tried setting up this in a Loaded event of the UserControl but the text is already there when the UserControl is fully loaded and showed.
Can someone please give an idea about how to achieve this. I checked this and this link but none seems to be working for me.
Thanks Deepak
If your loading is logical rather than physical you must handle it yourself.
Particularly you must have a way to tell the UserControl when the data are loaded.
Here is a full sample using MVVM (not perfect):
The UserControl:
<UserControl x:Class="WpfApplication1.LoadDataView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="boolToVizConverter"></BooleanToVisibilityConverter>
<local:ReverseBooleanToVisibilityConverter x:Key="reverseBoolToVizConverter"></local:ReverseBooleanToVisibilityConverter>
</UserControl.Resources>
<Grid>
<TextBlock Visibility="{Binding Model.IsLoadingData,Converter={StaticResource boolToVizConverter}}">...</TextBlock>
<TextBlock Visibility="{Binding Model.IsLoadingData,Converter={StaticResource reverseBoolToVizConverter}}">Data loaded</TextBlock>
</Grid>
</UserControl>
So "..." is displayed while we are loading (in a real application you would use a better UI control to render this status like a ProgressRing).
And when the data are loaded we display "Data loaded".
Code behind:
using System.Windows;
using System.Windows.Controls;
namespace WpfApplication1
{
public partial class LoadDataView : UserControl
{
public LoadDataViewModel Model
{
get { return (LoadDataViewModel)GetValue(ModelProperty); }
set { SetValue(ModelProperty, value); }
}
public static readonly DependencyProperty ModelProperty = DependencyProperty.Register("Model", typeof(LoadDataViewModel), typeof(LoadDataView));
public LoadDataView()
{
InitializeComponent();
this.DataContext = this;
}
}
}
The view model:
using System.ComponentModel;
namespace WpfApplication1
{
public class LoadDataViewModel : INotifyPropertyChanged
{
private bool isLoadingData = false;
public bool IsLoadingData
{
get { return isLoadingData; }
set
{
if (value != isLoadingData)
{
isLoadingData = value;
PropertyChanged(this, new PropertyChangedEventArgs("IsLoadingData"));
}
}
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
}
}
Here is the main view:
<StackPanel>
<ContentControl x:Name="content"></ContentControl>
<Button Click="Button_Click">Display data</Button>
</StackPanel>
When we click the Button we display the data UserControl and we trigger the loading:
private async void Button_Click(object sender, RoutedEventArgs e)
{
LoadDataViewModel model = new LoadDataViewModel { IsLoadingData = true };
content.Content = new LoadDataView { Model = model };
await Task.Delay(3000);
model.IsLoadingData = false;
}
The async/await and Task stuff is just there to simulate the loading of the data.
Finally here is the reverse bool to visibility converter:
using System;
using System.Windows;
using System.Windows.Data;
namespace WpfApplication1
{
public class ReverseBooleanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return !(bool)value ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

Listbox SelectedIndices of wpf window

how can I use the following code on WPF Listbox Window . It works fine on normal Win form Listbox , so i wanted test on WPF window, but im getting error saying
'System.Windows.Controls.ListBox' does not contain a definition for 'SelectedIndices' and no extension method 'SelectedIndices' accepting a first argument of type 'System.Windows.Controls.ListBox' could be found (are you missing a using directive or an assembly reference?)
this is the original code I have
private void listBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
try
{
this.textBox1.Text = people[listBox1.SelectedIndices[0]].name;
this.connectbox.Text = people[listBox1.SelectedIndices[0]].ipaddress;
}
catch
{
}
This is an example of what I mean by "WPF Mentality", which differs a lot from the archaic winforms mentality of mashing all together logic and UI:
<Window x:Class="WpfApplication4.Window11"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window11" Height="300" Width="300">
<DockPanel>
<Button DockPanel.Dock="Top" Content="Copy" Command="{Binding CopyCommand}"/>
<UniformGrid Columns="2">
<ListBox ItemsSource="{Binding Items}" SelectionMode="Extended">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding Mode=TwoWay, Path=IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
<ListBox ItemsSource="{Binding SelectedItems}"/>
</UniformGrid>
</DockPanel>
</Window>
Code Behind:
using System.Linq;
using BaseWPFFramework.MVVM;
using System.Collections.ObjectModel;
namespace WpfApplication4
{
public partial class Window11
{
public Window11()
{
InitializeComponent();
DataContext = new ListViewModel();
}
}
}
ViewModel:
public class ListViewModel: ViewModelBase
{
private ObservableCollection<Selectable<string>> _items;
public ObservableCollection<Selectable<string>> Items
{
get { return _items ?? (_items = new ObservableCollection<Selectable<string>>()); }
}
private ObservableCollection<string> _selectedItems;
public ObservableCollection<string> SelectedItems
{
get { return _selectedItems ?? (_selectedItems = new ObservableCollection<string>()); }
}
private DelegateCommand _copyCommand;
public DelegateCommand CopyCommand
{
get { return _copyCommand ?? (_copyCommand = new DelegateCommand(Copy)); }
}
private void Copy()
{
SelectedItems.Clear();
Items.Where(x => x.IsSelected).Select(x => x.Value).ToList().ForEach(SelectedItems.Add);
}
public ListViewModel()
{
Enumerable.Range(1, 100).Select(x => new Selectable<string>("Item" + x.ToString())).ToList().ForEach(x => Items.Add(x));
}
}
public class Selectable<T>: ViewModelBase
{
private T _value;
public T Value
{
get { return _value; }
set
{
_value = value;
NotifyPropertyChange(() => Value);
}
}
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
NotifyPropertyChange(() => IsSelected);
}
}
public Selectable(T value)
{
Value = value;
}
public Selectable(T value, bool isSelected): this(value)
{
IsSelected = isSelected;
}
public override string ToString()
{
return Value != null ? Value.ToString() : string.Empty;
}
}
Just copy and paste my code in a File -> New -> WPF Application and see the results for yourself.
Notice that I'm using a generic solution (Selectable<T>), so you could use that with any classes you want.
Also, please don't do things like: this.textBox1.Text = whatever in WPF. WPF encourages the separation of UI and data, and you must understand that UI Is Not Data in order to properly work with WPF. Please leave the winforms mentality behind if you expect good results from WPF.
Instead, either create a proper ViewModel to hold the data shown in your textboxes, or bind the textboxes directly to an instance of the SelectedItems as in my example.
As an aside, off-topic comment, the normal way is not the winforms way anymore. All recent (< 10 Years) technologies (WPF, Silverlight, WinRT) are all XAML-based and encourage the use of MVVM.
This means that the winforms way is now the old way, not the normal way.

Localize images in WPF

I'm building a program in WPF which must feature multi-language support, with the ability to switch language at run-time. My question concerns the image part of the localization.
I've built a solution which does not work the way I had hoped it would work and I would like some help fixing these problems. The code posted below is only a demonstration of the concept I'm trying to achieve. My real program has quite many pictures, so I want to avoid putting them all in a list, updating them one-by-one.
My idea is to name the images according to what language they belong to. The OriginalSource property (in lack of a better name) is formatted like "Koala.(lang).jpg", and the two images for English and French are called "Koala.en-GB.jpg" and "Koala.fr-FR.jpg".
My problem is that without the code which is commented at (1), the images will not be assigned a "real" Source (in the Image class).
Also, after having used the code at (1) (which violates my wish not to use an enumeration of all images), the "real" source is not updated at (2) at the click on the Button. My hopes were that (3) and (4) would solve these problems but apparently they don't.
Help would be much appreciated.
Code follows:
MainWindow.xaml (incorrect)
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
Title="MainWindow" Height="700" Width="525">
<Window.Resources>
<local:LanguageCodeSetter x:Key="CodeSetter" LanguageCodeValue="en-GB" />
</Window.Resources>
<StackPanel>
<local:LocalizedImage x:Name="imgKoala" LanguageCode="{Binding Source={StaticResource CodeSetter}, Path=LanguageCodeValue, Mode=OneWay}" OriginalSource="Koala.(lang).jpg" Height="300" Stretch="Uniform" />
<local:LocalizedImage x:Name="imgPenguins" LanguageCode="{Binding Source={StaticResource CodeSetter}, Path=LanguageCodeValue, Mode=OneWay}" OriginalSource="Penguins.(lang).jpg" Height="300" Stretch="Uniform" />
<Button Content="Don't click here!" Click="Button_Click" />
</StackPanel>
</Window>
MainWindow.xaml.cs (incorrect)
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
namespace WpfApplication1
{
public partial class MainWindow : Window
{
private string LanguageCodeResource
{
get
{
return ((LanguageCodeSetter)Resources["CodeSetter"]).LanguageCodeValue;
}
set
{
((LanguageCodeSetter)Resources["CodeSetter"]).LanguageCodeValue = value;
}
}
public MainWindow()
{
InitializeComponent();
//(1)
//imgKoala.OriginalSource = imgKoala.OriginalSource;
//imgPenguins.OriginalSource = imgPenguins.OriginalSource;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
LanguageCodeResource = "fr-FR";
//(2)
//imgKoala.LanguageCode = imgKoala.LanguageCode;
//imgPenguins.LanguageCode = imgPenguins.LanguageCode;
}
}
public class LocalizedImage : Image
{
public static readonly DependencyProperty LanguageCodeProperty = DependencyProperty.Register("LanguageCode", typeof(string), typeof(LocalizedImage));
public static readonly DependencyProperty OriginalSourceProperty = DependencyProperty.Register("OriginalSource", typeof(string), typeof(LocalizedImage));
public string LanguageCode
{
get
{
return (string)GetValue(LanguageCodeProperty);
}
set
{
SetValue(LanguageCodeProperty, value);
//(3)
SetValue(SourceProperty, new BitmapImage(new Uri(OriginalSource.Replace("(lang)", value), UriKind.RelativeOrAbsolute)));
}
}
public string OriginalSource
{
get
{
return (string)GetValue(OriginalSourceProperty);
}
set
{
SetValue(OriginalSourceProperty, value);
//(4)
SetValue(SourceProperty, new BitmapImage(new Uri(value.Replace("(lang)", LanguageCode), UriKind.RelativeOrAbsolute)));
}
}
}
public class LanguageCodeSetter : INotifyPropertyChanged
{
private string _languageCode;
public event PropertyChangedEventHandler PropertyChanged;
public string LanguageCodeValue
{
get
{
return _languageCode;
}
set
{
_languageCode = value;
NotifyPropertyChanged("LanguageCodeValue");
}
}
private void NotifyPropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
}
#NVM
Point taken about confusing names. I've updated my code.
The reason that I use an INotifyPropertyChanged object is that I want the changes in one variable, namely the resource called CodeSetter, to propagate to all instances of LocalizedImage. The reason for this is that I'm building a WPF application with quite a lot of images, and I do not want to be forced to add them all in a list in code-behind (thus forgetting to add some images, and making future refactoring of the application more tedious). At a click on the button, the value of "LanguageCode" does change in all instances of LocalizedImage, so the propagation part seems to work. However, setting the "real" source at (3) does not. I've also tried setting base.Source to the same value (new BitmapImage(...)) but with the same result.
The property (LanguageCodeResource) is only for brevity in the Button_Click event handler.
Maybe I'm aiming in the wrong direction to solve this problem? Additional feedback would be much appreciated.
#NVM
That did the trick. Thank you very much!
For anyone interested, I attach my correct code. The somewhat cumbersome DataContext datatype is because I need "two datacontexts" for my images and texts (which come from an XML file) in my real program.
MainWindow.xaml
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication2"
Title="MainWindow" Height="700" Width="525">
<Window.Resources>
<local:LocalizedImageSourceConverter x:Key="localizedImageSourceConverter" />
</Window.Resources>
<StackPanel x:Name="layoutRoot">
<Image x:Name="imgKoala" Source="{Binding Path=LanguageCode, Converter={StaticResource localizedImageSourceConverter}, ConverterParameter='Koala.(lang).jpg'}" Height="300" Stretch="Uniform" />
<Image x:Name="imgPenguins" Source="{Binding Path=LanguageCode, Converter={StaticResource localizedImageSourceConverter}, ConverterParameter='Penguins.(lang).jpg'}" Height="300" Stretch="Uniform" />
<Button Content="Don't click here!" Click="Button_Click" />
</StackPanel>
MainWindow.cs.xaml
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace WpfApplication1
{
public partial class MainWindow : Window
{
private string LanguageCodeValue
{
set
{
layoutRoot.DataContext = new
{
LanguageCode = value
};
}
}
public MainWindow()
{
InitializeComponent();
LanguageCodeValue = "en-GB";
}
private void Button_Click(object sender, RoutedEventArgs e)
{
LanguageCodeValue = "fr-FR";
}
}
public class LocalizedImageSourceConverter : IValueConverter
{
public object Convert(object values, Type targetType, object parameter, CultureInfo culture)
{
return ((string)parameter).Replace("(lang)", (string)values);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
Firstly you ought to stop using 'LanguageCode' to name just about everything. It is really confusing :D
Secondly for
<Window.Resources>
<local:LanguageCodeSetter x:Key="LanguageCode" LanguageCode="en-GB" />
</Window.Resources>
to make any sense
public string LanguageCode
{
get
{
return _languageCode;
}
set
{
_languageCode = value;
NotifyPropertyChanged("LanguageCode");
}
}
ought to be a dependency property and not a clr property backed by INotify...
EDIT
I still dont get how the LanguageCode property will work in the resources section.
Anyway having understood what you are trying to achieve here, there is a very simple solution
<Image Source="{Binding Path=LanguageCode, Converter={StaticResource localizedImageSourceConverter}, ConverterParameter='Koala.jpg'}"/>
public class LocalizedImageSourceConverter : IValueConverter
{
public object Convert(object values, Type targetType, object parameter, CultureInfo culture)
{
string fileName = Path.GetFileNameWithoutExtension((string)parameter);
string extension = Path.GetExtension((string)parameter);
string languageCode = (string)values;
return string.Format("{0}{1}{2}", fileName, languageCode, extension);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Instead of binding the Source property to a filePath(URI), I am binding it to the LanguageCode property. The LanguageCode property should be in your ViewModel or whatever datacontext object you are binding to.
The converter will take the path to the base image as a parameter and combines it with the bound LanguageCodeProperty to give you a localized path. And since you are binding to the LanguageCode property in your datacontext whenever it changes all images will be automatically updated. Note that the converter parameter cannot be bound. If you want to bind both the filePath and the language code use a multibinding.
*There might be syntax errors in the code, I am only trying to convey the concept

Categories