WPF Converters and ObservableCollections - wpf

I'm binding an ObservableCollection to a control which has a converter to change its visibility depending on if the collection has any values or not:
Simplified example:
XAML:
<Window.Resources>
<local:MyConverter x:Key="converter"/>
</Window.Resources>
<Grid x:Name="grid">
<Rectangle Height="100" Width="200" Fill="CornflowerBlue"
Visibility="{Binding Converter={StaticResource converter}}"/>
<Button Content="click"
HorizontalAlignment="Left" VerticalAlignment="Top"
Click="Button_Click"/>
</Grid>
C#:
ObservableCollection<string> strings;
public MainWindow()
{
InitializeComponent();
strings = new ObservableCollection<string>();
grid.DataContext = strings;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
strings.Add("new value");
}
When the collection is bound, the Rectangle is visible when there are values and not when the collection is empty. However, if the collection is empty and I add a value at runtime, the Rectangle does not appear (the converter's Convert method isn't even fired). Am I missing something or just trying to ask too much of IValueConverter?

OK, so here's how I got around the problem using a MultiValueConverter
The converter now looks like:
public object Convert(
object[] values,
Type targetType,
object parameter,
System.Globalization.CultureInfo culture)
{
ObservableCollection<string> strings =
values[0] as ObservableCollection<string>;
if (strings == null || !strings.Any())
return Visibility.Collapsed;
else
return Visibility.Visible;
}
public object[] ConvertBack(
object value,
Type[] targetTypes,
object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
And the XAML now looks like:
<Rectangle Height="100" Width="200" Fill="CornflowerBlue">
<Rectangle.Visibility>
<MultiBinding Converter="{StaticResource converter}">
<Binding Path="."/>
<Binding Path="Count"/>
</MultiBinding>
</Rectangle.Visibility>
</Rectangle>
The C# remains the same :)

I think the converter in a Binding is always called if the Binding source has been updated and notifies about that update (as a DependencyProperty or using INotifyPropertyChanged). However, an ObservableCollection does not raise the PropertyChanged event if an item has been added or removed, but it raises the CollectionChanged event. It does not raise any event at all if an item in the collection is changed. Even if the item itself raises PropertyChanged, this will not update the Binding on the collection since the Binding source is not the item, but the collection.
I fear your approach will not work this way. You could bind directly to ObservableCollection.Count and add an appropriate math converter to it to perform the inversion and multiplication, but the Count property does not perform change notification, so this no option. I think you will have to provide another property in your ViewModel or code-behind which handles these cases...
best regards,

You must set the DataContext after creating the collection; it is probable that you initialize the "strings"collection to "null", you set the DataContext in the constructor to that value(e.g. null), then you actually create the collection--this way, the DataContext remains null.
You must set the DataContext again after creating the collection.

Related

In WPF XAML, is it possible to bind a ViewModel property to a DependencyProperty defined in a Converter without using `x:Name`? [duplicate]

This question already has answers here:
Binding ConverterParameter
(3 answers)
Closed 27 days ago.
In the following xaml sample source, I am trying to bind the A property in the SampleViewModel to the B property, which is a DependencyProperty in the SampleConverter.
However, when I do this, I get a XAML bind error and the Data Context is displayed as null.
I know it is possible to get the Data Context using x:Name, but is it possible to get the Data Context without using x:Name?
<Window
x:Class="WpfApp1.BindPage"
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:local="clr-namespace:WpfApp1"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Window.DataContext>
<local:SampleViewModel />
</Window.DataContext>
<StackPanel>
<StackPanel.Height>
<Binding Path="ActualWidth" RelativeSource="{RelativeSource Mode=Self}">
<Binding.Converter>
<local:SampleConverter B="{Binding A}" />
</Binding.Converter>
</Binding>
</StackPanel.Height>
</StackPanel>
</Window>
I should mention that with RelativeSource I could not get other than myself (in this case, other than the SampleConverter).
That isn't how you use converters.
I don't know what local:SampleConverter is exactly but it has a property B
B="{Binding A}"
The binding provides the value, you may also bind a command parameter
Here's an example
<Image Name="GridImage"
Visibility="{Binding AppSettings.ShowGrid
, Converter={StaticResource BooleanToVisibilityConverter}}"
/>
This is going to set the Visibility property, the binding is to AppSetting.ShowGrid which is in the datacontext rather than converter. The BooleanToVibilityConverter is taking a bool from ShowGrid and converts it to a Visibility.
If you wanted to bind multiple properties then you can use a multiconverter with a multibinding.
<MultiBinding Converter="{ui:AllMultiplyMultiConverter}" StringFormat="{}{0:n0}">
<Binding Path="Value" ElementName="turnTime"/>
<Binding Path="MoveRate"/>
</MultiBinding>
A multiconverter receives an array object[] for those values.
Since markup extension and ivalueconverter are not dependency objects you would need to reference the parent object to use a dependency property.
You could add dependency properties to your window and reference them in a markup extension that's also a valueconverter.
Consider this value converter that's also a markup extension.
public class AddConverter : MarkupExtension, IValueConverter
{
public double ValueToAdd { get; set; }
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
double bound = (Double)value;
return bound + ValueToAdd;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
ValueToAdd is just a property, you can't bind it.
You can get a reference in that ProvideValue to the parent dependency object. Hence window or usercontrol.
IProvideValueTarget target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
That target is your control and you can grab dependency properties off that. You could (say) set ValueToAdd in there and use it in the converter. You could cast the datacontext of the targetobject and read values off properties directly.
This is a very complicated approach. I've never had the need for this myself and I would recommend multibinding and multiconverter instead.

ObservableCollection Remove() not firing to Visibility binding

I have a strange issue with my WPF project. I have a ObservableCollection<T> bound to a ListBox. When I add and remove items, the binding works and the list displays the correct results.
The issue I have, is I'm also binding this same property to another XAML control, but it doesn't trigger the converter when I remove an item from the list. It works when I add items.
The relevant XAML is
<view:WelcomeView Visibility="{Binding Steps, Converter={StaticResource CollapseIfZero}}"/>
<ListBox ItemsSource="{Binding Steps}" />
And the converter is
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var col = value as ICollection;
return col.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
}
I have a break point in the converter. When a new item is added, the break point is hit. When an existing item is removed, the break point is not hit.
Does WPF do something magical with the ListBox which I'm not aware of (which has led to this unexpected behavior)?
ObservableCollection implements INotifyCollectionChanged and ListBox (and other ItemsControls) listens when collection was modified.
Steps property itself doesn't change, it is the same ObservableCollection.
WelcomeView.Visibility is bound to Steps, and doesn't update because property value didn't change, it keeps the same object reference.
try create binding to Steps.Count property (converter should be modified to use int value)
<view:WelcomeView Visibility="{Binding Steps.Count, Converter={StaticResource CollapseIfZeroCount}}"/>
or
there is bool HasItems property in ItemsControl. I would make a binding with ElementName and BooleanToVisibilityConverter
<view:WelcomeView "{Binding ElementName=Lst, Path=HasItems, Converter={StaticResource Bool2Visibility}}"/>
<ListBox Name="Lst" ItemsSource="{Binding Steps}" />

Set Multibing for text - get and set

I would like to bind my TextBox.Text to two different sources.
I have 2 ViewModels, one is general ViewModel and one is more specific (which is inherit from its parent).
Both ViewModels has a property called "Hotkey".
I would like to bind my TextBox.Text so it will get the value from the general ViewModel and set it to the specific ViewModel.
I tried the following:
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" TextAlignment="Center" Foreground="#000">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource test}">
<Binding Path="DataContext.Hotkey" RelativeSource="{RelativeSource AncestorType={x:Type MetroStyle:MetroWindow}}" Mode="OneWay" />
<Binding Path="Hotkey" Mode="OneWayToSource"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
It does get the value from the general ViewModel, but it doesn't set its value to the specific one (which inherits from the parent)
I believe the problem may be in the Converter you used for MultiBinding, I've just tried a simple demo and looks like that Converter should be implemented like this:
public class TestConverter : IMultiValueConverter
{
private bool justConvertedBack;
object IMultiValueConverter.Convert(object[] values, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
if (justConvertedBack) {
justConvertedBack = false;
return Binding.DoNothing;
}
return values[0];
}
object[] IMultiValueConverter.ConvertBack(object value, Type[] targetTypes,
object parameter, System.Globalization.CultureInfo culture)
{
justConvertedBack = true;
return new object[] {null, value};
}
}
It happens that after the ConvertBack has been done, the Convert will be triggered and keep the Text of your TextBox unchanged (although you tried deleting/modifying it before). So we need some flag justConvertedBack here to prevent that from occurring.
Currently changing the source from the general ViewModel will change the TextBox's Text but does not update the source from the specific ViewModel. However if setting/typing some value for the TextBox's Text will update the source from the specific ViewModel but won't reflect that value back to the source from the general ViewModel. I hope that behavior is what you want.

WPF Binding nested objects

I'm very new to WPF and I'm having difficulties binding a request object with nested objects which was derived from a WSDL to a XAML textbox. Programmatically I was able to bind to a textbox but I would like to understand the syntax needed to bind via XAML. Once I have some direction It'll make it much easier to research a full solution. Thanks
The ResultSet and Message Object will always be [0].
Code
MainWindow()
{
InitializeComponent();
GetMarketingMessagesResponse request = new GetMarketingMessagesResponse();
request = (GetMarketingMessagesResponse)XMLSerializerHelper.Load(request, #"C:\SSAResponse.xml");
DataContext = request;
Binding bind = new Binding();
bind.Source = request.ResultSet[0].Message[0];
bind.Path = new PropertyPath("SubjectName");
this.txtbSubject.SetBinding(TextBox.TextProperty, bind);
}
The return value in the Visual Studio Watch bind.Source = request.ResultSet[0].Message[0];
is
bind.Source = {GetMarketingMessagesResponseResultSetMessage} which is the class name.
XAML
I'm looking for direction on how to bind to this class and the properties inside
<TextBox Name="txtbMessageDetails" HorizontalAlignment="Right" Margin="0,50.08,8,0" TextWrapping="Wrap" Text="{Binding Source=ResultSet[0].Message[0], Path=SubjectName}" VerticalAlignment="Top" Height="87.96" Width="287.942"/>
Use a converter which will receive the request and extract the message.
<Window.Resources>
<local:MessageExtractorConverter x:Key="messageExtractorConverter" />
</Window.Resources>
<TextBox Name="txtbMessageDetails" HorizontalAlignment="Right" Margin="0,50.08,8,0" TextWrapping="Wrap" Text="{Binding Converter={StaticResource messageExtractorConverter}" VerticalAlignment="Top" Height="87.96" Width="287.942"/>
Converter implementation:
public class MessageExtractorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var val = value as GetMarketingMessagesResponse;
if (val != null)
{
// You can modify this code to extract whatever you want...
return val.ResultSet[0].Message[0];
}
else
{
return null;
}
}
You already put the request object in your DataContext, making it the default Source for all bindings. So, instead of specifying another Source (which would just override the DataContext), you use the binding's Path to make your way from the DataContext to the property you need:
<TextBox Name="txtbMessageDetails" Text="{Binding Path=ResultSet[0].Message[0].SubjectName}" />
Here is an article explaining how the DataContext works, and how it is "inherited" from control to control in your Window: http://www.codeproject.com/Articles/321899/DataContext-in-WPF

WPF : Conditional templating of textblock

I have a bunch of textblocks in an itemscontrol... I need to know how can I underline the text in the textblock based on whether the text is available in a list in the data model..
Sounds very simple to me...but I have been googling since the past 8 hrs...
Can I use datatriggers and valueconverters for this purpose? If yes, then how can I execute the method which lies in the viewModel (the method which helps me to check whther a given a text exists in the data model list)...
Even if I go for conditional templating....how do I access the list which lies in my model (the viewmodel can fetch it...but then how do i access the viewmodel?)..
This should be a fairly easy thing to do...Am I really missing something very simple here?? :)
I am following the MVVM pattern for my application..
One way is to use a multivalueconverter which is a class that implements IMultiValueConverter. A multivalueconverter allows you to bind to several values which means that you can get a reference to both your viewmodel and the text of your TextBlock in your valueconverter.
Assuming that your viewmodel has a method called GetIsUnderlined that returns true or false indicating whether or not the text should be underlined your valueconverter can be implemented along these lines:
class UnderlineValueConverter : IMultiValueConverter
{
#region IMultiValueConverter Members
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var viewmodel = values[0] as Window1ViewModel;
var text = values[1] as string;
return viewmodel.GetIsUnderlined(text) ? TextDecorations.Underline : null;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
You can use this valueconverter in the following way for a TextBlock:
<Grid x:Name="grid1" >
<Grid.Resources>
<local:UnderlineValueConverter x:Key="underlineValueConverter" />
</Grid.Resources>
<TextBlock Text="Blahblah">
<TextBlock.TextDecorations>
<MultiBinding Converter="{StaticResource underlineValueConverter}">
<Binding /> <!-- Pass in the DataContext (the viewmodel) as the first parameter -->
<Binding Path="Text" RelativeSource="{RelativeSource Mode=Self}" /> <!-- Pass in the text of the TextBlock as the second parameter -->
</MultiBinding>
</TextBlock.TextDecorations>
</TextBlock>
</Grid>

Resources