Bind string from .resx ResourceDictionary to TextBlock.Text using index key - c#

i'm trying to get my view in different languages, using Properties/Resources.resx file for localization.
I've my model looks like the following:
class City
{
public int Id { get; set; }
public string LocalizationKey { get; set; }
}
The ViewModel:
class ViewModel
{
public ObservableCollection<City> Cities { get; set; }
}
And on my View, i've the following code:
<ItemsControl ItemsSource="{Binding Cities}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding LocalizationKey}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
When i want to get the value of my string key from the dictionary for only one item without Items Collection it works correctly by using the following code:
<TextBlock Text="{x:Static properties:Resources.MyStringKey}" />
The problem is when using the code above with an ItemsControl where the keys are unknowns! Is there any way to access to the dictionary values by using the LocalizationKey as an index?

Can you do something like:
public class City
{
public int Id { get; set; }
public string LocalizationKey { get; set; }
public City(string englishName)
{
LocalizationKey = Properties.Resources.ResourceManager.GetString(englishName);
}
}
I'm not sure this best practice; but it what came to mind first.

After hours of web searching, i finally found a solution by using a converter, it may not the best practices to solve the problem but at least it does exactly what i want:
My Converter:
public class LocalizationConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var key = value as string;
if (!string.IsNullOrEmpty(key))
{
string dictionaryValue = Resources.ResourceManager.GetString(key);
return dictionaryValue ?? key;
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
and the XAML code:
<TextBlock Text="{Binding LocalizationId, Converter={StaticResource LocalizationConverter}}" />
Thanks.

Related

How to access property of an object inside a list in a DataGrid in XAML

I've been searching for a while, but I'm thinking maybe I am looking in the wrong direction. I have a DataGrid where the ItemsSource is set to a list of Book objects. At the same time, the Book object contains a list of Chapter objects.
public class Book {
public string Title;
public List<Chapter> AvailableChapters;
}
public class Chapter {
public int ChapterNumber;
public int NumberOfPages;
}
I use them this way in a DataGrid:
<DataGrid SelectionMode="Extended"
HeadersVisibility="Column"
CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Title" Binding="{Binding Title}" />
<DataGridTextColumn Header="Available chapters" Binding="{Binding AvailableChapters}" />
<!-- This is what I tried
<DataGridTemplateColumn Header="Available chapters">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ListBox ItemsSource="{Binding AvailableChapters}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>-->
</DataGrid.Columns>
</DataGrid>
My aim is to show the list of chapters inside the book, as a list of strings, where each string is the chapter number (ChapterNumber property). For this reason, I was trying to access in some way the ChapterNumber property in the ChapterList property. Tried to use even the ListBox or AvailableChapters.ChapterNumber, but it makes no sense).
Example:
Edit
Tried with a converter but, although the value is not null, it says its length is zero.
public class ChapterListToStringListConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
List<Chapter> chapters = value as List<Chapter>;
if (chapters == null)
{
chapters = new List<Chapter>();
}
Debug.Print(chapters.Count.ToString()); // "0"
Debug.Print(chapters[0].ChapterNumber); // Error: index out of range
string result = string.Join(" ", chapters.Select(chap => chap.Name));
return result;
}
}
}
In case you are adding Chapters after the view was initialized, AvailableChapters should be an ObservableCollection:
public class Book
{
public string Title { get; set; }
public ObservableCollection<Chapter> AvailableChapters { get; }
= new ObservableCollection<Chapter>();
}
In your converter just check if value is an IEnumerable:
public object Convert(
object value, Type targetType, object parameter, CultureInfo culture)
{
var chapters = value as IEnumerable<Chapter>;
return chapters == null
? "-"
: string.Join(" ", chapters.Select(chap => chap.ChapterNumber));
}
The source for data-binding needs to be a property, not a field as you currently have.
To get a single string with all of the available chapters, just add a calculated property to your Book class, which combines all of the chapter items from the list.
public class Book
{
public string Title {get; set;}
public List<Chapter> AvailableChapters; {get;}
public string AvailableChaptersDisplay
{ get {return string.Join( " ", AvailableChapters.Select( c => c.ChapterNumber )); } }
}
You could add a property that calculates that for you.
public class Book {
public string Title;
public List<Chapter> AvailableChapters;
public string AvailableChaptersString
{
get {return list_concatenated_in_desired_format; }
}
}

DataBind a Dictionary value to ObservableCollection C# - XAML

I've got a Observable collection of custom objects and a public dictionary variable.
I would like the "BrandName" attribute to act as the Key for the "Brands" dictionary and bind the colour to the button. How would I go about doing this? The dictionary variable is outside of the class.
C# Code:
private ObservableCollection<BusService> BusServicesGUI;
public Dictionary<String, Brush> Brands;
public MainWindow(Dictionary<String, BusService> busServices)
{
InitializeComponent();
BusServicesGUI = new ObservableCollection<BusService>(BusServices.Values);
lstMachineFunctions.ItemsSource = BusServicesGUI;
lstMachineFunctions.Items.Refresh();
}
C# Class:
public class BusService
{
public string ServiceNumber { get; set; }
public string BrandName { get; set; }
public List<Location> Locations { get; set; }
public BusService(string brandName, string serviceNumber)
{
BrandName = brandName;
ServiceNumber = serviceNumber;
Locations = new List<Location>();
}
}
XAML CODE:
<StackPanel x:Name="ServiceStack">
<ItemsControl x:Name="lstMachineFunctions">
<ItemsControl.ItemTemplate >
<DataTemplate>
<Grid HorizontalAlignment="Stretch">
<usercontrols:BusServiceCard/>
<Button Tag="{Binding ServiceNumber}" Background="{Binding Brands[BrandName]}" Height="50" Click="ButtonCl"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
As you can see from the XAML my current attempts have been trying Background="{Binding Brands[BrandName]}" This has however not worked, any help would be much appreciated.
You can use an IValueConverter to pefrom this operation.
public class BrandColorConverter : IValueConverter
{
public Dictionary<String, Brush> Brands = new Dictionary<string, Brush>()
{
{ "brand1", Brushes.Red },
{ "brand2", Brushes.Blue }
};
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (!(value is BusService))
return Binding.DoNothing;
var busService = (BusService)value;
if (!Brands.ContainsKey(busService.BrandName))
return Binding.DoNothing;
return Brands[busService.BrandName];
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
In xaml, add it as a static resuorce:
<Window.Resources>
<local:BrandColorConverter x:Key="BrandColorConverter"/>
</Window.Resources>
And use it in your button:
<Button Tag="{Binding ServiceNumber}"
Background="{Binding Converter={StaticResource BrandColorConverter}}"
Height="50"
Click="ButtonCl"/>
This binding goes to the current element, so the whole BusService object will be passed to the converter.
Hope it solves your problem.
I would strongly advise you to look into MVVM pattern if you are going to use WPF with data binding, as it makes things much more streamlined.

Binding the same Collection to ComboBox and Datagrid

GitHub Link: https://github.com/babakin34/wpf_test/tree/master/WpfApp1
I have the following classes:
VIEWMODELS:
public class PersonVM : BindableBase
{
public int ID { get; set; }
private string _lastName;
public string LastName
{
get { return _lastName; }
set { SetProperty(ref _lastName, value); }
}
}
public class MainVM : BindableBase
{
public ObservableCollection<PersonVM> People { get; set; }
private PersonVM _selectedPerson;
public PersonVM SelectedPerson
{
get { return _selectedPerson; }
set { SetProperty(ref _selectedPerson, value); }
}
public MainVM()
{
People = new ObservableCollection<PersonVM>()
{
new PersonVM()
{
ID = 1,
LastName = "AA"
},
new PersonVM()
{
ID = 2,
LastName = "BB"
},
};
SelectedPerson = People.First();
}
}
VIEW:
<Grid>
<StackPanel>
<ComboBox ItemsSource="{Binding People}"
SelectedItem="{Binding SelectedPerson}"
DisplayMemberPath="LastName"
Margin="0,5,0,25"/>
<DataGrid ItemsSource="{Binding People}"/>
</StackPanel>
</Grid>
How can I achieve that the "MainVM.SelectedPerson" from ComboBox is notified when user selects the empty element, which is caused by the Datagrid's default last entry?
PS: I am using Prism, but the problem is not Prism related. You can replace BindableBase by INotifyPropertyChanged.
Edit:
The real issue here is a bit different than i thought at first. When selecting the insert row, you see in the output window that WPF is unable to cast a "NamedObject" into a "PersonVM". The Datagrid creates a NamedObject for the insert row to work with, but the binding simply does not work since it's of the wrong type.
The clean and easy solution is to create a converter to check if the object is of type PersonVM, otherwise return null (and not the NamedObject instance).
public class MyConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is PersonVM)
return value;
return null;
}
}
And in the xaml
<DataGrid x:Name="DataGrid"
ItemsSource="{Binding People}"
SelectedCellsChanged="DataGrid_OnSelectedCellsChanged"
SelectedItem="{Binding SelectedPerson,
Converter={StaticResource myConverter}}"
Old and dirty
Not the nicest way, but if you don't mind using the viewmodel (or abstracting it via an interface) in the codebehind you can use the "SelectionChanged" event to set the SelectedPerson in the ViewModel to null if any of the selected items are not of the type you need.
private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
bool invalidStuffSelected = false;
//throw new System.NotImplementedException();
foreach (var obj in DataGrid.SelectedItems)
{
if (!(obj is PersonVM))
invalidStuffSelected = true;
}
MainVM vm = (MainVM) this.DataContext;
if (invalidStuffSelected)
vm.SelectedPerson = null;
}
In your example the selectionmode "single" would make more sense since the combobox can only show one selected value.

WPF UserControl based on generic object properties

I have an object based on byte data with over 200 properties that I care about, in the sense that I want to (1) know the value, and (2) know when the value changed from one message to the next.
A snippet of the XAML I am using:
<Label Content="Prop Name" />
<TextBlock Text="{Binding PropName}"
Background="{Binding PropName,
Converter={StaticResource CompareToLastValueConverter}}" />
Currently, I have these lines pasted for EACH property, with appropriate grid location settings.
My question is this: is there a nice way to create a nested WPF UserControl that takes a generic object property from the model and handles assigning the name (with spaces) to the Label, then assigning the value of the property to the TextBlock like the example above?
Also, is this the best way to think about this problem, or am I missing a link in the "WPF way" of doing things?
I've often wanted to try this. I'd create an ItemsControl template for PropertyInfo.
I created a test class:
public class MyClass
{
public string PropertyTest1 {get;set;}
public string PropertyTest2 { get; set; }
public string PropertyTest3 { get; set; }
public string PropertyTest4 { get; set; }
}
To display the properties of. In my data context for the display, I've got two things to bind to. A list of PropertyInfos, and the object in question. Since the PropertyInfo is static, you might be able to do this a better way using a converter or something, and not need to bind it to a property:
public PropertyInfo[] Properties
{
get { return typeof(MyClass).GetProperties(); }
}
public MyClass MyObject
{
get { return new MyClass { PropertyTest1 = "test", PropertyTest3 = "Some string", PropertyTest4 = "Last Property" }; }
}
Now, displaying the properties is easy:
<ItemsControl x:Name="PropertyDisplay" ItemsSource="{Binding Properties}" Grid.IsSharedSizeScope="True">
<ItemsControl.Resources>
<local:PropertyInfoValueConverter x:Key="PropertyInfoValueConverter"/>
</ItemsControl.Resources>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}" Margin="4,2"/>
<TextBlock Grid.Column="1" Margin="4,2"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
But these are 'static', we can't bind to any values. A way to get around that is to use the Tag property, and a multi-binding converter:
So lets add Tag="{Binding MyObject}" to our ItemsSource, and throw that and the PropertyInfo into a value converter for our second textblock:
<TextBlock Grid.Column="1" Margin="4,2">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource PropertyInfoValueConverter}">
<Binding Path=""/>
<Binding ElementName="PropertyDisplay" Path="Tag"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
The converter is actually pretty simple, especially since you're not using text-boxes (so only going the read-only direction):
public class PropertyInfoValueConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
PropertyInfo propertyInfo = values[0] as PropertyInfo;
return propertyInfo.GetValue(values[1]);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
This is the result:
You say you want spaces for the names, that could be done with a converter with some logic looking for whatever naming convention you've got (spaces before capital letters?).
It would be fun to play with template selectors to choose boolean, string, float templates and treat them differently. (Checkboxes, text, 00.00 formatted text etc)
Edit: Exploring Template Selector
Here's a sample template selector:
public class PropertyInfoTemplateSelector : DataTemplateSelector
{
public DataTemplate StringTemplate { get; set; }
public DataTemplate IntegerTemplate { get; set; }
public DataTemplate DecimalTemplate { get; set; }
public DataTemplate BooleanTemplate { get; set; }
public DataTemplate DefaultTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
PropertyInfo propertyInfo = item as PropertyInfo;
if (propertyInfo.PropertyType == typeof(string))
{
return StringTemplate;
}
else if (propertyInfo.PropertyType == typeof(int))
{
return IntegerTemplate;
}
else if (propertyInfo.PropertyType == typeof(float) || propertyInfo.PropertyType == typeof(double))
{
return DecimalTemplate;
}
else if (propertyInfo.PropertyType == typeof(bool))
{
return BooleanTemplate;
}
return DefaultTemplate;
}
}
Our ItemsControl is now simply:
<ItemsControl x:Name="PropertyDisplay" ItemsSource="{Binding Properties}"
Grid.IsSharedSizeScope="True"
Tag="{Binding MyObject}"
ItemTemplateSelector="{StaticResource PropertyInfoTemplateSelector}"
Margin="20"/>
I also added spaces in names using this converter:
public class PropertyInfoNameConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string text = value as string;
if (string.IsNullOrWhiteSpace(text))
return string.Empty;
StringBuilder newText = new StringBuilder(text.Length * 2);
newText.Append(text[0]);
for (int i = 1; i < text.Length; i++)
{
if (char.IsUpper(text[i]))
if ((text[i - 1] != ' ' && !char.IsUpper(text[i - 1])) ||
(char.IsUpper(text[i - 1]) &&
i < text.Length - 1 && !char.IsUpper(text[i + 1])))
newText.Append(' ');
newText.Append(text[i]);
}
return newText.ToString();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
(Credit to this: https://stackoverflow.com/a/272929/1305699).
Updating our class to contain some boolean and fload fields:

Bind to bound item?

I have Comments class, to which I am binding:
<ListBox ItemsSource="{Binding CommentFiles}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Text}" TextWrapping="Wrap"/>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding UserId}"/> <!-- Here should be username -->
<TextBlock Text=","/>
<TextBlock Text="{Binding CreatedAt}"/>
</StackPanel>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
As you can see Comments class have UserId property, which is just some char combination. I can get User class object with asynchronous getUser(userID) method.
I want to see user's username(which is in User class) instead of UserId, when I bind to comments.
And I cannot edit classes. Is there a way to do this?
You could Bind the userId in combination with a value converter that takes the userId, calls getUser(value) and returns the user name.
<TextBlock Text="{Binding UserId, Converter={StaticResource MyUserIdConverter}" />
And the value converter would look like:
public class MyUserIdConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// Add some checks here ;-)
return GetUser((string) value);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
A little example based on my comment. You can either load the complete user, or only the username.
public class ExtendedCommentFile
{
private readonly CommentFile _comment;
public ExtendedComment(CommentFile comment)
{
_comment = comment;
}
public int UserId
{
get { return _comment.UserId; }
set { _comment.UserId = value; }
}
public User User
{
get { return LoadTheUserHereOrInVM(); }
}
public string Username
{
get { return LoadTheUserNameHereOrInVM(); }
}
}
/// <summary>
/// This is the unchangeable commentfile class
/// </summary>
public class CommentFile
{
public string Text { get; set; }
public int UserId { get; set; }
public DateTime CreatedAt { get; set; }
}
/// <summary>
/// This is unchangeable user class
/// </summary>
public class User
{
}

Categories

Resources