Why is this WPF custom DependencyProperty binding on value converter not executing? - c#

I am building what I have learned is basically an accordion control, with a selection mode that ensures that only one section is open at the time. Each section is implemented using Expander controls, so if on Expander is opened, all others should close.
I have done that in the following manner:
Added a property representing the Id of the currently open section, ActiveQuestionId on the view model (which implements INotifyPropertyChanged)
Created a converter inheriting from DependencyObject that is able to convert the ActiveQuestionId to a boolean indicating whether a specific section should be open, by adding a DependencyProperty ControlValue to the converter that indicates which section that it belongs to
Creating a local converter for each section with ControlValue bound to the QuestionId of its section
While the converter methods executes successfully, the problem is the DependencyProperty ControlValue is never set even though it binds successfully to a value and don't raise any errors. I have confirmed this through various debugging. So the result is that all sections are stuck with the default value, rendering the accordion selection behavior I want, useless.
Why is the DependencyProperty binding being ignored? Is it because it is defined within a binding, or something else?
Remarks
Everything is data-driven, and worked great in static a mockup I did before implementing the generic data-driven version. A fully data driven solution is a must, so using one way multi bindings or hardcoded XAML parameters (the solutions I have been able to find for related issues) is not an option.
It is important to note that all other bindings work perfect, so there is no problem DataContext wise. As everything should work (in my mind), this is also why I have not gone the WPF Toolkit Accordion way yet, so please do not suggest this initially (unless it is really the only way). First of, being new to WPF, I would like to understand why this is not working.
XAML (extract - some names changed to obfuscate business meaning - central part is IsExpanded binding):
<ItemsControl ItemsSource="{Binding QuestionSection.QuestionAssignments}"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
Style="{x:Null}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="{StaticResource Grey400Brush}"
BorderThickness="0 1 0 0">
<Expander Background="{StaticResource Grey200Brush}"
Foreground="Black"
Padding="0"
Margin="0">
<Expander.IsExpanded>
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType=views:TypeOfParentControl}"
Path="DataContext.ActiveQuestionId"
Mode="TwoWay">
<Binding.Converter>
<converters:TestConverter ControlValue="{Binding QuestionId}"/>
</Binding.Converter>
</Binding>
</Expander.IsExpanded>
<Expander.HeaderTemplate>
<!--Custom Styling Here, All Bindings Work-->
</Expander.HeaderTemplate>
<!--Content Here, All Bindings Work-->
</Expander>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
Converter (simplified)
public class TestConverter : DependencyObject, IValueConverter
{
public static readonly DependencyProperty ControlValueProperty = DependencyProperty.Register("ControlValue", typeof(short), typeof(TestConverter), new PropertyMetadata(default(short)));
public short ControlValue
{
get { return (short) GetValue(ControlValueProperty); }
set { SetValue(ControlValueProperty, value); }
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (short)value==ControlValue;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ? ControlValue : Binding.DoNothing;
}
}
ActiveQuestionId implementation in ViewModel - INotifyPropertyChanged is tested and works, ViewModel is DataContext on Parent UserControl
private short activeQuestionId;
public short ActiveQuestionId
{
get
{
return activeQuestionId;
}
set
{
if (value != activeQuestionId)
{
activeQuestionId = value;
OnPropertyChanged();
}
}
}

The current DataContext value is not inherited down to the TestConverter instance.
You may avoid this complex binding altogether and implement your control by using a ListBox:
<ListBox ItemsSource="{Binding QuestionSection.QuestionAssignments}"
SelectedValuePath="QuestionId"
SelectedValue="{Binding ActiveQuestionId}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Expander IsExpanded="{Binding IsSelected,
RelativeSource={RelativeSource TemplatedParent}}">
...
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>

Related

WPF DataGridTextColumn FontWeight Binding / Value Converter not working

I understand the concept of Binding / Value converter well, but for some reasons the following binding doesn't work. I would like the change the FontWeight to Bold for some Descriptions (Description is a text field):
XAML:
<DataGridTextColumn Header="Description"
Binding="{Binding Description}"
FontWeight="{Binding Description, Converter={converters:DescriptionToFontWeightConverter}}"/>
Value converter method (simplified):
public class DescriptionToFontWeightConverter : ConverterMarkupExtension<DescriptionToFontWeightConverter>
{
public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// Some logic based on the value
// ..
return "Bold"; // I believe I should use "Bold", and not "FontWeights.Bold" here (like it would be with a dependency property, but the problem is that it doesn't go inside the method.
}
public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => Binding.DoNothing;
}
I believe the problem comes from the binding path of the FontWeight property. For example, if I replace (Path=)Description by RelativeSource={RelativeSource Self}, it goes inside the value converter method, but I don't think I can retrieve the value of the binding.. I think it isn't something abnormal (at least something I didn't expect), but I wonder if I shouldn't replace the DataGridTextColum by a DataGridTemplateColum and digs further?
I use the ConvertMarkupExtension method from this website which doesn't require to specify the value converters as static resources.
UPDATE with DataGridColumnTemplate
<DataGridTemplateColumn Header="Description">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Description}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="FontWeight"
Value="{Binding Description, Converter={converters:DescriptionToFontWeightConverter}}"/>
</Style>
</TextBlock.Style>
</TextBlock>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox Text="{Binding Description}"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
Thanks for any insights :-)
A DataGridColumn doesn't inherit any DataContext so trying to bind to a Description property of the current item will always fail.
The Binding property is special. Its type is Binding and the binding that you define in XAML will eventually be applied to the element that gets created at runtime. In the case of a DataGridTextColumn, this is a TextBlock or a TextBox depending on whether you are in edit mode.
but I wonder if I shouldn't replace the DataGridTextColum by a DataGridTemplateColum and digs further
If you use a DataGridTemplateColumn and define a TextBlock in the CellTemplate (and a TextBox in the CellEditingTemplate), you can actually bind to a property of the current item as usual. This is because the element in the template is added to the element tree and inherits a DataContext like any other element.

Procedurally generating text with inline <span> XAML from a binding

I am building a C# WPF application using MVVM to decouple the view from the business logic.
One of my data sources is a legacy application that outputs ANSI color codes and I would like to faithfully reproduce these in the UI using textblocks, or boxes, or whatever is most appropriate.
I have been able to write a very simple converter to turn the ANSI codes into elements with styles to set the color:
"\x1b[30mblack\x1b[37mwhite"
becomes
#"<span style=""color:#000000"">black<span style=""color:#BBBBBB"">white</span></span>"
However I can not find a way to bind this text to my views while also reproducing the colors.
Most examples online focus on situations where the text is always going to be the same, so colors can be hardcoded in the XAML and the text is bound to different spans/runs. This is not going to work for me.
I have looked briefly at using the WebBrowser control but this seems like a very big hammer for such a small problem. I'm going to have hundreds of these labels in a list, so performance is a concern.
Finally I found a solution whereby XAML may be written into the view at runtime, but I have had no luck whatsoever getting my converted string to load as valid XAML: Richtextbox wpf binding
This seems to be one of those cases of WPF making a simple problem extremely difficult. Is there a good solution that I am overlooking?
What exactly are you trying to parse here? Your text says you're trying to parse XAML yet the code you've provided is HTML?
If you can stick with XAML then it's relatively straightforward. First of all you'll need some XAML data in your view model:
public string[] Spans { get; } = new string[]
{
"<Span Foreground=\"Blue\">Hello World!</Span>",
"<Span Foreground=\"Green\">Goodbye World!</Span>"
};
Whenever you have a list of things to draw in WPF you usually use an ItemsControl. However, instead of a list you'll probably want a WrapPanel. A converter can be used to convert each element of the list into a span which you can wrap that in a parent ContentControl:
<ItemsControl ItemsSource="{Binding Spans}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Content="{Binding Path=., Converter={StaticResource SpanConverter}}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Then all you need is the converter itself to take the raw XAML, parse it and return the resulting object. You'll also need to add the default (empty) namespace along with "x" so that the parser know where to find the objects it's deserializing:
public class SpanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var context = new ParserContext();
context.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
context.XmlnsDictionary.Add("x", "http://schemas.microsoft.com/winfx/2006/xaml");
return XamlReader.Parse(value?.ToString(), context) as ContentElement;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return Binding.DoNothing;
}
}
Result:
Now take all this and throw it out, because parsing raw XAML is not how you do this in MVVM. To do this properly you would create a view model for your spans like so:
public class SpanViewModel
{
public string Text { set; get; }
public Color Foreground { set; get; }
// .. plus any other fields you want
}
And you would create a list of those instead:
public SpanViewModel[] Spans { get; } = new SpanViewModel[]
{
new SpanViewModel{Text="Hello World!", Foreground=Colors.Blue},
new SpanViewModel{Text="Goodbye World!", Foreground=Colors.Green}
};
We're going to use a DataTemplate, so get rid of the ItemTemplate from your ItemsControl:
<ItemsControl ItemsSource="{Binding Spans}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
And create a DataTemplate for your SpanViewModel, binding to the relevant properties (you'll need to use TextBlock because Span doesn't support binding):
<Window.Resources>
<DataTemplate DataType="{x:Type local:SpanViewModel}">
<TextBlock Text="{Binding Text}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Foreground">
<Setter.Value>
<SolidColorBrush Color="{Binding Foreground}" />
</Setter.Value>
</Setter>
</Style>
</TextBlock.Style>
</TextBlock>
</DataTemplate>
</Window.Resources>
There are many variations on this, but this should be enough to get you going.

Inherited DataContext - bindings not working

Following situation: I got a base class which provides a little framework for making modal dialogs with an adorner.
The base class has a property of type DataTemplate which contains the actual input scheme (all kinds of input are possible) as well as an object property which contains the mapping model (a model class to which the template binds it's input values).
Because I want to reuse the adorner, I made it have a ContentControl which anon has a ContentTemplate with the actual dialog design. The dialog's design contains a ContentControl whose Template is bound to the property in the adorner class. The DataContext of the adorner's ContentControl is set to itself, of course.
Now the embedded ContentControl (in the design) generates the DataTemplate and displays (in the current case) a TextBox. This TextBox now should be bound to the model. Therefore I reused the DataContext of the adorner design template for the actual input template. Here's how I've done it:
The adorner's ControlTemplate
<Border Grid.Row="0" Background="{DynamicResource InputAdornerHeaderBackground}" BorderThickness="0,0,0,1" CornerRadius="0">
<TextBlock HorizontalAlignment="Stretch" VerticalAlignment="Stretch" TextWrapping="Wrap" Text="{Binding Header}"
FontSize="{DynamicResource InputAdornerHeaderFontSize}" Foreground="{DynamicResource InputAdornerHeaderForeground}"
FontWeight="{DynamicResource InputAdornerHeaderFontWeight}" Margin="8" />
</Border>
<Border Grid.Row="1" BorderThickness="1,0,1,1" BorderBrush="{DynamicResource InputAdornerBorderBrush}" CornerRadius="0">
<ContentControl ContentTemplate="{Binding InputControlTemplate}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
</Border>
</Grid>
</ContentTemplate>
The actual input template (DataTemplate)
<DataTemplate x:Key="TextInputTemplate">
<Grid Background="Black" DataContext="{Binding DataContext.InputMapping, RelativeSource={RelativeSource AncestorType={x:Type ContentControl}}}">
<TextBox Text="{Binding Path=Text, Mode=OneWayToSource}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" BorderThickness="0"
adorners:InputAdorner.FocusElement="{Binding RelativeSource={RelativeSource Self}}" />
</Grid>
</DataTemplate>
Model class
public sealed class TextInputModel
{
public string Text { get; set; }
}
Adorner properties
public DataTemplate InputControlTemplate
{
get { return _inputControlTemplate; }
private set
{
if (Equals(value, _inputControlTemplate)) return;
_inputControlTemplate = value;
OnPropertyChanged();
}
}
public object InputMapping
{
get { return _inputMapping; }
private set
{
if (Equals(value, _inputMapping)) return;
_inputMapping = value;
OnPropertyChanged();
}
}
FYI: The model is being dynamically instantiated when the Adorner is being created. It does not get set twice. This must be some kind of binding issue.
The template shows correctly. I see and can input stuff into the textbox, but once I fetch the model all properties are default (""). It did work one or two times but somehow design changes have obviously made it disfunctional.
I don't get what is interfering here as from my point of view all should be set up correctly. I checked the context of the DataTemplate: It is the actual model class. Yet the textbox inputs do not update the property.
EDIT:
For some reason it seems that the attatched property is causing this issue. But why is it interfering? It does not override the DataContext, does it?

Confusion about WPF binding

I am trying to bind a 2D array of buttons arranged in stackpanels to a 2D ObservableCollection...
Yet, I'm afraid I don't understand something very elementary about binding.
My XAML:
<Window.Resources>
<DataTemplate x:Key="ItemsAsButtons">
<Button Content="{Binding}" Height="100" Width="100"/>
</DataTemplate>
<DataTemplate x:Key="PanelOfPanels">
<ItemsControl ItemsSource="{Binding Path=DayNumbers}" ItemTemplate=" {DynamicResource ItemsAsButtons}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
</Window.Resources>
...
<ItemsControl x:Name="DaysPanel" Grid.ColumnSpan="7" Grid.Row="2"
ItemTemplate="{DynamicResource PanelOfPanels}"/>
My C# code:
The backend:
/// <summary>
/// Window BE for Calendar.xaml
/// </summary>
public partial class Calendar : Window
{
private CalendarViewModel _vm;
public Calendar()
{
InitializeComponent();
_vm = new CalendarViewModel();
this.DataContext = _vm;
}
}
The ViewModel:
class CalendarViewModel
{
CalendarMonth _displayedMonth;
EventCalendar _calendar;
public CalendarViewModel()
{
_displayedMonth = new CalendarMonth();
}
public ObservableCollection<ObservableCollection<int>> DayNumbers
{
get
{
return _displayedMonth.DayNumbers;
}
}
}
I'm trying to populate the buttons with values from CalendarViewModel.DayNumbers - yet the buttons do not appear. I'm clearly doing something wrong with my binding.
Change all your DynamicResource to StaticResource. This shouldn't stop it working, but might be inefficient at runtime. Have a look this page for WPF resources overview.
Also your ItemsControl is not bound to DayNumbers. Add a binding like so:
<ItemsControl x:Name="DaysPanel" Grid.ColumnSpan="7" Grid.Row="2"
ItemTemplate="{StaticResource PanelOfPanels}"
ItemsSource={Binding DayNumbers}/>
When you set the DataContext on Calendar window you set which object will be the default binding source for the whole window. You didn't specify which property of your ViewModel is bound to the ItemsControl. This is what the code above does.
EDIT Because you are overriding the item template for the ItemsControl and provide a collection container there, you need to provide the ItemsSource for it as well. The syntax {Binding} simply means bind to each member or enumeration, in this case ObservableCollection<int>.
Just to reiterate, the template is exactly that - a template for displaying data. It should be reusable, you should be able to bind it to whatever model you want. A rule of thumb - the data binding to actual data should happen on the control, not the template.
Like Igor said, you need specify ItemsSource={Binding DayNumbers} in outer-most ItemsControl, otherwise, it binds to the DataContext, which is CalendarViewModel and it is not IEnumerable.
Once you do that, it will apply <DataTemplate x:Key="PanelOfPanels"> for each item inside DayNumbers. Note that the DataContext of the DataTemplate in each element in DayNumbers, which is of type ObservableCollection<int>. Here you cannot specify ItemsSource="{Binding Path=DayNumbers}" as DayNumbers is not a valid property in ObservableCollection<int>. Instead, since ObservableCollection<int> is already a IEnumerable, it should be fine not specifying ItemsSource since it will by default bind to DataContext.
Finally, it goes to your inner-most <DataTemplate x:Key="ItemsAsButtons">, and you can put button there as what you did.
Hope it clarifies a little bit. Sorry I don't have the environment to test it out and give you the solution.
Debugging WPF bindings is not straightforward. One tip is you can use dummy converter and set breakpoint in the Convert method to see what it binds.
public class DebugConverter1 : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value;
}
#endregion
}
{Binding Converter={StaticResource debugConverter1}}

Exposing Multiple Databinding sources

I feel like I'm missing a fairly fundamental concept to WPF when it comes to databinding, but I can't seem to find the right combination of Google keywords to locate what I'm after, so maybe the SO Community can help. :)
I've got a WPF usercontrol that needs to databind to two separate objects in order to display properly. Both objects must be dynamically set from an outside source. Thus far I've simply been using the DataContext property of the form for dynamic object binding, but that only allows for one object to be referenced. I feel like this is a simple problem and that I must be missing something obvious.
My previous attempt looks something like this:
<UserControl.Resources>
<src:Person x:Key="personSource" />
<src:Job x:Key="jobSource" />
</UserControl.Resources>
<TextBox Text="{Binding Source={StaticResource personSource}, Path=Name" />
<TextBox Text="{Binding Source={StaticResource jobSource}, Path=Address" />
This will bind to any defaults I give the classes just fine, but If I try to dynamically set the objects in code (as I show below) I don't see any change.
Person personSource = FindResource("personSource") as Person;
personSource = externalPerson;
Job jobSource= FindResource("jobSource") as Job;
jobSource = externalJob;
What am I missing?
I would probably use a CustomControl with two DependencyProperties. Then the external site that uses your custom control could bind the data that they want to that control, also by using a custom control you can template the way the control looks in different situations.
Custom control code would look something like:
public class CustomControl : Control
{
public static readonly DependencyProperty PersonProperty =
DependencyProperty.Register("Person", typeof(Person), typeof(CustomControl), new UIPropertyMetadata(null));
public Person Person
{
get { return (Person) GetValue(PersonProperty); }
set { SetValue(PersonProperty, value); }
}
public static readonly DependencyProperty JobProperty =
DependencyProperty.Register("Job", typeof(Job), typeof(CustomControl), new UIPropertyMetadata(null));
public Job Job
{
get { return (Job) GetValue(JobProperty); }
set { SetValue(JobProperty, value); }
}
static CustomControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomControl), new FrameworkPropertyMetadata(typeof(CustomControl)));
}
}
Generic.xaml is a file that should be created for you and could have a Style that looks something like this:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication3">
<Style TargetType="{x:Type local:CustomControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:CustomControl}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<StackPanel>
<TextBox Text="{TemplateBinding Person.Name}" />
<TextBox Text="{TemplateBinding Job.Address}" />
</StackPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
Finally, when you go to use your control you would do something like this.
<src:CustomControl Person="{Binding Person}" Job="{Binding Job}" />
The reason your text boxes don't update is that you are binding them to a StaticResource. As the name implies these resources are static and don't post change notifications. And because Binding is a MarkupExtension and does not derive from DependencyObject you can't use a DynamicResource.
Try creating depedency properties on your control to reference the Person and Job objects.
Then set the DataContext of the UserControl to reference itself.
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Then you can use dot notation to reference the required properties.
<TextBox Text="{Binding Path=Person.Name" />
<TextBox Text="{Binding Path=Job.Address" />
Or use the source parameter
<TextBox Text="{Binding Source=Person, Path=Name" />
<TextBox Text="{Binding Source=Job, Path=Address" />

Categories

Resources