I have a WPF Expander in my project. I have applied styles to the expander to change the header colors and whatnot. After doing so, my data is still bound to the Header's content area, but it just binds the raw data and not the formatting I have specified. See the examples below.
<Style x:Key="ValkyrieStyleExpander" TargetType="{x:Type Expander}">
<!-- Ommiting property setters for brevity -->
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<TextBlock Text="{Binding}" FontWeight="Bold"
Foreground="White" VerticalAlignment="Center" />
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
And here is the actual declaration with the appropriate binding syntax
<Expander Style="{StaticResource ValkyrieStyleExpander}"
Margin="10,10,0,0"
Width="670"
Header="{Binding PolicyNumber, StringFormat=Policy {0}}">
</Expander>
We I run the app, the header should display "Policy 123456", and before I restyled the expander it did so. But now when I run the app, the header just shows "123456". I am still kind of a babe-in-the-woods when it comes to databinding, so I am not sure really what I need to do to get the new style to show the correct data. Hopefully the answer isn't to add it to the ValkyrieStyleExpender's Header Template style, as that would defeat the purpose of having a style (Not all expanders in the project are for displaying a particular policy)
StringFormat usually does not work when using it within the Expander.Header property since the property is not of type string.
You will need to write your own class derived from IFormatter that implements the formatted string you'd actually define in the property. I've researched quite a bit and found no better solution for this issue.
The class may look as follows:
public class SomeClass: IFormattable
{
public string ToString(string format, IFormatProvider formatProvider)
{
if(format == "n")
{
return "This is the formatted string";
}
else
{
return "this is the non-formatted string";
}
}
}
And you would use it in your style this way:
Setter Property="HeaderStringFormat" Value="n" />
If the string format is not working for you, you can easily implement a converter, such as...
#region PolicyConverter (ValueConverter)
public class PolicyConverter : MarkupExtension, IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value != null)
{
return "Policy " + value.ToString();
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
#endregion
...and then declare it in your Xaml...
<Window.Resources>
<pc:PolicyConverter x:Key="PolicyConverter"/>
</Window.Resources>
...and finally reference it in your template. I did not use (or modify) your ValkyrieStyleExpander code. But to verify the correctness of this approach, I used the following declaration as a functional prototype...
<Expander Name="Expander1">
<Expander.HeaderTemplate>
<DataTemplate>
<TextBlock
Text="{Binding ElementName=Expander1,
Path=DataContext.PolicyNumber,
Converter={StaticResource PolicyConverter}}"/>
</DataTemplate>
</Expander.HeaderTemplate>
</Expander>
...and it worked as expected (.net 4.5). If you needed to evacuate the in-lined templating to a global declaration in your Xaml, this would also make a great starting point.
Related
I have simple Enum:
public enum StatusMessage
{
Cancel,
Done,
[Description("In process...")]
InProcess,
[Description("We have delay...")]
Delay,
Waiting
}
And GridViewColumn:
My property:
StatusMessage StatusMsg;
XAML:
<GridViewColumn Width="180" Header="Status" >
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding StatusMsg}" Foreground="{Binding StatusMsg,Converter={my:StatusMessageToColorConverter}}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
And i have this EnumToStringConverter:
public class EnumToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
string EnumString;
try
{
EnumString = Enum.GetName((value.GetType()), value);
return EnumString;
}
catch
{
return string.Empty;
}
}
// No need to implement converting back on a one-way binding
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Now i want to use this Convertor inside my TextBlock :
<TextBlock Text="{Binding StatusMsg, Converter={my:EnumToStringConverter}}" Foreground="{Binding StatusMsg,Converter={my:StatusMessageToColorConverter}}" />
So the problem is that i have this error:
'my:EnumToStringConverter' is used like a markup extension but does
not derive from MarkupExtension.
What is this MarkupExtension ?
You need to declare an instance of the EnumToStringConverter in XAML. It can be a local resource or declared in app.xaml to make it accessible everywhere.
<Window.Resources>
<my:EnumToStringConverter x:Key="DefaultEnumToStringConverter"/>
</Window.Resources>
Then use it like this:
Text="{Binding StatusMsg, Converter={StaticResource DefaultEnumToStringConverter}}"
Note the word "StaticResource" in the converter. That is the markup extension. This one tells WPF to go find the static resource with the key "DefaultEnumToStringConverter". WPF will search up the visual tree of the element looking for a resource with that key. If one isn't found it will check at the application level in app.xaml.
MarkupExtensions are the things at the beginning of an attribute enclosed in the {}, "x", "binding", "static", etc. They are what give WPF the ability to resolve the text attribute in to a useful object instance. You can create your own MarkupExtensions to do some pretty cool things.
In your particular example it is complaining because it is looking for a markup extension named "my", from the inner Converter={my:EnumToStringConverter}.
In my main Window I have a MenuItem and a UserControl. I would like to disable/enable the MenuItem if one of the TextBoxes inside the UserControl is empty/not empty respectively.
Given a UserControl named ContactDetails and a TexBox called ContactNameTextBox, here's my xaml code for the MenuItem:
<MenuItem x:Name="DeleteContact"
Header="Delete Contact"
IsEnabled="{Binding ElementName=ContactDetails.ContactNameTextBox,Path=Text.Length, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
For some reason, the MenuItem always stays enabled. What am I missing?
You are binding to the length of the Text but you need a Converter from length to a bool, because IsEnabled property expects a bool.
public class NumToBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value!=null && value is int )
{
var val = (int)value;
return (val==0) ? false : true;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value!=null && value is bool )
{
var val = (bool)value; return val ? 1 : 0;
}
return null;
}
}
Add a local xmlns for this and a resource.
xmlns:local="clr-namespace:YourNamespace"
and this is the reference to the converter class.
<local:NumToBoolConverter x:Key="NumToBoolConverter"/>
In your Binding section add this :
Converter={StaticResource NumToBoolConverter}
This can be your final MenuItem definition:
<MenuItem x:Name="DeleteContact"
Header="Delete Contact"
IsEnabled="{Binding ElementName=ContactDetails.ContactNameTextBox,
Path=Text.Length,
Converter={StaticResource NumToBoolConverter},
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}"/>
There are a couple of problems with your binding. The first is that you specified a two-way binding. That implies that you want to write back to the 'length' property in your textbox. Since it is readonly you can't.
Normally you should get an error for this:
A TwoWay or OneWayToSource binding cannot work on the read-only
property 'Length' of type 'System.String'.
Now strangely enough, the binding does work after that. But that is REALLY not the right way. The magic of .NET is allowing a 0 to be interpreted as 'false'. But it is not a safe binding. As Olaru said in his answer, the length property is an integer and the IsEnabled field is looking for a bool. What if you wanted to bind to the 'visibility' property?
So what is the best way to handle this then? Converters are definitely one choice, and in many cases the best choice. The advantage to converters is that they can be re-used in similar cases. We have a library full of converters that we use very often. Olaru has described how to do that, so I won't repeat what he has already said.
In some cases though, it is beneficial to know a different way. A datatrigger will allow you to do the same kind of thing as a converter. It is a one-way binding. Here is an example.
<MenuItem x:Name="DeleteContact" Header="Delete Contact">
<MenuItem.Style>
<Style TargetType="{x:Type MenuItem}" BasedOn="{StaticResource {x:Type MenuItem}}">
<Setter Property="MenuItem.IsEnabled" Value="true"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Text.Length, ElementName=ContactNameTextBox}" Value="0">
<Setter Property="MenuItem.IsEnabled" Value="false"/>
</DataTrigger>
</Style.Triggers>
</Style>
</MenuItem.Style>
</MenuItem>
No code necessary!!
There are plenty of arguments about the pros and cons of converters and datatriggers. But the main thing is to know that there are more than one way to do what you are asking.
I am working on a knowledgebase application which manages articles.
An article consists of a header (Id, Author ... etc.) and a set of text fragments, one of which contains the Title (TextType==2).
There is a listbox intended to display Id and Title but I have been unable correctly to bind the title to a textblock.
I have working code elsewhere to load a title entity
ArticleText te = _header.ArticleTexts.Where(at => at.TextType == 2).FirstOrDefault();
so the property of the entity yielding the title would be te.Body
I set the ItemsSource of my listbox in code
public ObservableCollection<ArticleHeader> HeaderCollection
{
get
{
return (ObservableCollection<ArticleHeader>)articlesListBox.ItemsSource;
}
set
{
articlesListBox.ItemsSource = value;
}
}
which correctly displays the Id but cannot seem to work out a way to bind to the (lazy loaded) title string.
My (simplified) xaml is as follows
<ListBox Name="articlesListBox">
<ListBox.ItemTemplate>
<DataTemplate>
<DockPanel>
<TextBlock DockPanel.Dock="Left" Text="{Binding Path=Id}"/>
<TextBlock TextWrapping="WrapWithOverflow" Margin="0,0,2,0"/>
</DockPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I am aware there may be a number of solutions, but what is the next step ?
Same as you did for ID?
<TextBlock Text={Binding Path=Body}/>
or
<TextBlock Text={Binding Path=Body.Title} />
Might have misunderstood your issue...
The
ArticleTexts.Where(TextType==2)
part is something that should happen in your viewmodel/controller, not in XAML.
Instead of binding your ListBox to a collection of ArticleHeader objects you can create a new collection of anonymous objects that only hold the information you need (id and title), expose it as a property and bind your ListBox to that.
Edit:
There's always the alternative of using a BindingConverter, but that would entail creating a brand new class, which would be even more of a hassle than creating a new collection. Maybe you should add the solution you have in mind in your question and explicitly ask whether a better alternative exists.
I cannot help thinking there must be other (better) ways, but I was able to achieve what I asked by defining a value converter.
[ValueConversion(typeof(ArticleHeader), typeof(String))]
public class HeaderToTitleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
ArticleHeader ah = value as ArticleHeader;
ArticleText textEntity = null;
using (KnowledgeManagementEntities ctx = KBContext.NewContext())
{
ctx.ArticleHeaders.Attach(ah);
textEntity = ah.ArticleTexts.Where(at => at.TextType == KBConstants.TITLE_TYPE).FirstOrDefault();
}
if (textEntity == null)
return "";
if (String.IsNullOrEmpty(textEntity.Body))
return "";
return textEntity.Body;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
Then in App.Xaml
<Application x:Class="SupportKB.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml"
xmlns:local="clr-namespace:SupportKB" xmlns:Properties="clr-namespace:SupportKB.Properties">
<Application.Resources>
<ResourceDictionary>
<local:HeaderToTitleConverter x:Key="headerToTitleConverter" />
</ResourceDictionary>
</Application.Resources>
</Application>
so the xaml now looks like this
<DataTemplate>
<DockPanel Margin="0,2">
<TextBlock DockPanel.Dock="Left" Text="{Binding Path=Id}" Width="50" />
<TextBlock TextWrapping="WrapWithOverflow"
Margin="0,0,2,0"
Text="{Binding Converter={StaticResource headerToTitleConverter}}"/>
</DockPanel>
</DataTemplate>
In the end, though, I am going to rework the design so that a list of titles is master, and the headers are selected from that.
Hello
I'm trying to change several controls' property according to some environment variables and i want to avoid creating a property for each control in the datacontext, so i thought using a converter which sets the property according to control name. Goal is to use one property for all controls:
<Grid.Resources>
<local:NameToStringConverter x:Key="conv" />
</Grid.Resources>
<TextBlock Name="FordPerfect"
Text="{Binding ElementName="FordPerfect" Path=Name, Converter={StaticResource conv}, Mode=OneWay}"/>
<TextBlock Name="Arthur"
Text="{Binding ElementName="Arthur" Path=Name, Converter={StaticResource conv}, Mode=OneWay}"/>
<TextBlock Name="ZaphodBeeblebrox"
Text="{Binding ElementName="ZaphodBeeblebrox" Path=Name, Converter={StaticResource conv}, Mode=OneWay}"/>
and ...
public class NameToStringConverter : IValueConverter
{
public object Convert(
object value, Type targetType,
object parameter, CultureInfo culture)
{
if (MyGlobalEnv.IsFlavor1 && ((string)value).Equals("ZaphodBeeblebrox")) return "42"
if (MyGlobalEnv.IsFlavor2 && ((string)value).Equals("ZaphodBeeblebrox")) return "43"
if (MyGlobalEnv.IsFlavor1 && ((string)value).Equals("Arthur")) return "44"
return "?";
}
public object ConvertBack(
object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new NotSupportedException("Cannot convert back");
}
}
I'm sure there's a better and more elegant way... Any ideas?
The point of oneway databinding is just to decouple UI (XAML) from code (CS). Here, your code and UI are tied so tightly together that trying to do this through databinding is really not buying you anything. You might simplify things by writing a method that takes the data value and applies it correctly to each control - still tightly coupled (bad) but at least the code is condensed and easy to follow (less bad).
What you should probably do though is not rely on the control name but define a ConverterParameter. See the bottom 1/3 of this article http://www.switchonthecode.com/tutorials/wpf-tutorial-binding-converters
You may bind directly to environment variable in your situation :
<Window xmlns:system="clr-namespace:System;assembly=mscorlib" ...>
<TextBlock Text="{Binding Source={x:Static system:Environment.OSVersion}}"/>
I am trying to bind to an integer property:
<RadioButton Content="None"
IsChecked="{Binding MyProperty,
Converter={StaticResource IntToBoolConverter},
ConverterParameter=0}" />
and my converter is:
[ValueConversion(typeof(int), typeof(bool))]
public class IntToBoolConverter : IValueConverter
{
public object Convert(object value, Type t, object parameter, CultureInfo culture)
{
return value.Equals(parameter);
}
public object ConvertBack(object value, Type t, object parameter, CultureInfo culture)
{
return value.Equals(false) ? DependencyProperty.UnsetValue : parameter;
}
}
the problem is that when my converter is called the parameter is string. i need it to be an integer. of course i can parse the string, but do i have to?
thanks for any help
konstantin
Here ya go!
<RadioButton Content="None"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<RadioButton.IsChecked>
<Binding Path="MyProperty"
Converter="{StaticResource IntToBoolConverter}">
<Binding.ConverterParameter>
<sys:Int32>0</sys:Int32>
</Binding.ConverterParameter>
</Binding>
</RadioButton.IsChecked>
</RadioButton>
The trick is to include the namespace for the basic system types and then to write at least the ConverterParameter binding in element form.
For completeness, one more possible solution (perhaps with less typing):
<Window
xmlns:sys="clr-namespace:System;assembly=mscorlib" ...>
<Window.Resources>
<sys:Int32 x:Key="IntZero">0</sys:Int32>
</Window.Resources>
<RadioButton Content="None"
IsChecked="{Binding MyProperty,
Converter={StaticResource IntToBoolConverter},
ConverterParameter={StaticResource IntZero}}" />
(Of course, Window can be replaced with UserControl, and IntZero may be defined closer to the place of actual usage.)
Not sure why WPF folks tend to be disinclined towards using MarkupExtension. It is the perfect solution for many problems including the issue mentioned here.
public sealed class Int32Extension : MarkupExtension
{
public Int32Extension(int value) { this.Value = value; }
public int Value { get; set; }
public override Object ProvideValue(IServiceProvider sp) { return Value; }
};
If this markup extension is defined in XAML namespace 'local:', then the original poster's example becomes:
<RadioButton Content="None"
IsChecked="{Binding MyProperty,
Converter={StaticResource IntToBoolConverter},
ConverterParameter={local:Int32 0}}" />
This works because the markup extension parser can see the strong type of the constructor argument and convert accordingly, whereas Binding's ConverterParameter argument is (less-informatively) Object-typed.
Don't use value.Equals. Use:
Convert.ToInt32(value) == Convert.ToInt32(parameter)
It would be nice to somehow express the type information for the ConverterValue in XAML, but I don't think it is possible as of now. So I guess you have to parse the Converter Object to your expected type by some custom logic. I don't see another way.