WPF Aggregating Commands using Multibinding in MVVM - wpf

I'm examining the solution of Leonid's command aggregating using multibinding in MVVM. https://www.codeproject.com/Articles/990113/MultiBinding-for-WPF-Command-Combining?msg=5666640#xx5666640xx I'm having issues with this solution when using it in a menu. I've added a simple menu with two items: item1 and item2. If menuitem item1 is selected then both menuitems item1 and item2 fire which is not what I want. Another aspect which I don't understand is if the XAML section which contains <Button.Command> is NOT commented then all four North, West, South and East commands fire when selecting menuitem item1. The multibinding for the button doesn't seem to be tied strictly to the button and is available to other controls. Any thoughts?
I'm aware of another command aggregate solution by Josh Smith but I read that his solution doesn't completely fit the MVVM context.
MainWindow XAML
<Window x:Class="MultiCommandButtonNoParams.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ice="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options"
xmlns:vm="clr-namespace:MultiCommandButtonNoParams.ViewModels"
xmlns:bc="clr-namespace:MultiCommandButtonNoParams.BindingConverters"
Title="MultiCommandButtonNoParams" Height="350" Width="525">
<Window.Resources>
<vm:ViewModel x:Key="viewModel" />
<bc:MultiCommandConverter x:Key="multiCommandConverter"/>
</Window.Resources>
<Window.DataContext>
<StaticResource ResourceKey="viewModel"/>
</Window.DataContext>
<Grid ShowGridLines="False" Background="Ivory">
<Grid.RowDefinitions>
<RowDefinition Height="0.5*"></RowDefinition>
<RowDefinition Height="3*"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<Menu Focusable="False">
<MenuItem Header="Menu">
<MenuItem Header="item1">
<MenuItem.Command>
<MultiBinding Converter="{StaticResource multiCommandConverter}" >
<Binding Path="NorthActionCommand"/>
</MultiBinding>
</MenuItem.Command>
</MenuItem>
<MenuItem Header="item2">
<MenuItem.Command>
<MultiBinding Converter="{StaticResource multiCommandConverter}" >
<Binding Path="WestActionCommand"/>
</MultiBinding>
</MenuItem.Command>
</MenuItem>
</MenuItem>
</Menu>
</StackPanel>
<StackPanel Grid.Row="1">
<TextBlock VerticalAlignment="Top" TextAlignment="Center" TextWrapping="Wrap" LineHeight="53">
<TextBlock.Inlines>
<Run Text="{Binding Path=NorthCommandManifest}" FontWeight="Bold" FontSize="18" Style="{StaticResource runForeground}" />
<LineBreak/>
<Run Text="{Binding Path=WestCommandManifest}" FontWeight="Heavy" FontSize="18" FontStyle="Italic" Style="{StaticResource runForeground}"/>
<LineBreak/>
<Run Text="{Binding Path=SouthCommandManifest}" FontWeight="ExtraBold" FontSize="18" FontStyle="Oblique" Style="{StaticResource runForeground}"/>
<LineBreak/>
<Run Text="{Binding Path=EastCommandManifest}" FontWeight="DemiBold" FontSize="18" FontStyle="Normal" Style="{StaticResource runForeground}"/>
</TextBlock.Inlines>
</TextBlock>
</StackPanel>
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Margin="10,2,10,2" Style="{StaticResource buttonBackground}" Focusable="False">
<!--<Button.Command>
Multicommand construction that consists of a set of sequentially executed commands.
Each command sends a message about execution to the TextBlock defined above.
<MultiBinding Converter="{StaticResource multiCommandConverter}" >
<Binding Path="NorthActionCommand"/>
<Binding Path="WestActionCommand"/>
<Binding Path="SouthActionCommand"/>
<Binding Path="EastActionCommand"/>
</MultiBinding>
</Button.Command>-->
<TextBlock FontWeight="Heavy" FontSize="18" TextAlignment="Center" TextWrapping="Wrap" Style="{StaticResource buttonForeground}">
Multi Command Button
</TextBlock>
</Button>
</Grid>
</Grid>
MultiCommandConverter.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Windows.Data;
using MultiCommandButtonNoParams.Commands;
namespace MultiCommandButtonNoParams.BindingConverters
{
public class MultiCommandConverter : IMultiValueConverter
{
private List<object> _value = new List<object>( );
/// <summary>
/// dobbin of the converter
/// </summary>
/// <param name="value">commands binded by means of multibiniding</param>
/// <returns>compound Relay command</returns>
public object Convert( object[ ] value, Type targetType,
object parameter, CultureInfo culture )
{
_value.AddRange( value );
return new RelayCommand( GetCompoundExecute( ), GetCompoundCanExecute( ) );
}
/// <summary>
/// here - mandatory duty
/// </summary>
public object[ ] ConvertBack( object value, Type[ ] targetTypes,
object parameter, CultureInfo culture )
{
return null;
}
/// <summary>
/// for execution of all commands
/// </summary>
/// <returns>Action<object> that plays a role of the joint Execute</returns>
private Action<object> GetCompoundExecute( )
{
return ( parameter ) =>
{
foreach ( RelayCommand command in _value )
{
if ( command != default( RelayCommand ) )
command.Execute( parameter );
}
};
}
/// <summary>
/// for check if execution of all commands is possible
/// </summary>
/// <returns>Predicate<object> that plays a role of the joint CanExecute</returns>
private Predicate<object> GetCompoundCanExecute( )
{
return ( parameter ) =>
{
bool res = true;
foreach ( RelayCommand command in _value )
if ( command != default( RelayCommand ) )
res &= command.CanExecute( parameter );
return res;
};
}
}
}

I would just like to know why all commands in 'MenuItem` fire when only one is selected
It's because your implementation of MultiCommandConverter is flawed:
public object Convert( object[ ] value, Type targetType,
object parameter, CultureInfo culture )
{
_value.AddRange( value );
return new RelayCommand( GetCompoundExecute( ), GetCompoundCanExecute( ) );
}
Every time the converter object is asked to convert the input bindings to a new ICommand object, it will add the multiple values passed to it to its internal list of commands and then return a new RelayCommand() object that simply calls delegates that refer to those commands. I.e. because the delegate instances returned by GetCompoundExecute() and GetCompoundCanExecute() capture the _value field, changes that occur later to the list referred to by that field are reflected in delegates that were created earlier.
Then, you create this converter as a resource, without specifying x:Shared=false. This means every place you used the converter is using the same object. So by the time all of the XAML is parsed, you have one converter that combines all of the commands used for all places where you've used that converter.
One fix might be to go ahead and specific x:Shared=false in the resource. But IMHO that's not really a good way to do it. For one, it means that now you've got a trap in your converter and have to remember that you need to specify that any time you put the converter in a resource dictionary. For another, you have another bug in the converter, where you add the values to the list every time the Convert() method is called. Which means that if you try to bind to values that may get updated from time to time, the list will get longer and longer, and will never eliminate the old values.
You could fix this by resetting the list every time the Convert() method is called, i.e. call _value.Clear(). But it's my opinion that that's still a design flaw. A converter should convert. It should not itself play a part in the result, as your implementation does here.
Alternatives that IMHO would be preferable include:
Writing a CompoundRelayCommand() that incorporates the GetCompoundExecute() and GetCompoundCanExecute() functionality, which you create a new instance of every time the Convert() method is called.
Aggregate the input values in the Convert() method itself by creating a new multicast delegate that chains the input commands, and then pass that multicast delegate to the RelayCommand().
Either of those approaches would be much better than just updating what you have now by fixing the "list isn't cleared" bug and using x:Shared="false" in the resource dictionary.

Related

Rookie questions on WPF MVVM and user controls

I am beginning WPF, and I am having a bit of a hard time implementing data binding.
Specifically, I have created a simple user control which holds a Label and a Button.
For this user control, I have created a ViewModel which holds just two properties, string "Text" and SimpleEnum "Status".
The point of the control is to display a status of something, like "Connected" yes/no, etc. The background color of the button indicates the status.
My XAML looks something like this
<Control.DataContext>
<vm:OnOffStatusViewModel />
</Control.DataContext>
<Label x:Name="label1" Height="Auto" HorizontalAlignment="Left" Content="{Binding Text}" Width="280" />
<Button Style="{StaticResource GlassButton}" Height="14" Width="14" Background="{Binding Status}" Grid.Column="1" />
with xmlns:vm="clr-namespace:Controls"
The code-behind has a property ViewModel exposing the view model, implementing INotifyPropertyChanged, and initializes as _viewModel = (OnOffStatusViewModel) DataContext;
Now, in my view that is using this control, I have managed to set the Text to something, as I in my implementing view code-behind have onOffStatus1.ViewModel.Text = ..., however, the status is set by enum, and is as such not really bindable to the background property of the button.
My questions related to this:
Is the way I have done the control correct? If not, what is the proper way of implementing data binding in user controls?
How can I have my enum status update the background property of the button using binding?
How can I have my enum status update the background property of the button using binding?
It's recommended to use a value converter for this task, returning a brush for every possible value of the enumeration. This way, your view model does not need to know anything about colors or brushes, and you can use the converter wherever you would like to visualize the status.
XAML
<UserControl x:Class="WpfApplication1.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1">
<UserControl.Resources>
<local:StatusColorConverter x:Key="StatusColorConverter" />
</UserControl.Resources>
<Button Background="{Binding Status, Converter={StaticResource StatusColorConverter}" />
</UserControl>
Converter
public enum Status
{
Connected
}
public class StatusColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
switch ((Status)value)
{
case Status.Connected: return new SolidColorBrush(Colors.Green);
}
return new SolidColorBrush(Colors.Black);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Is the way I have done the control correct? If not, what is the proper
way of implementing data binding in user controls?
Your implementation seems fine to me. You might want to eliminate the coupling between the view model and the view (which currently holds a reference to the view model) via dependency injection. But this depends on your use-cases and the architecture you want to use.
I would take a slightly different approach than the other answers here, I like to put the code and logic into my view models directly, so here's how I would do it:
<Control.DataContext>
<vm:OnOffStatusViewModel />
</Control.DataContext>
<Label x:Name="label1" Height="Auto" HorizontalAlignment="Left" Content="{Binding Text}" Width="280" />
<Button Style="{StaticResource GlassButton}" Height="14" Width="14" Background="{Binding ButtonBg}" Grid.Column="1" />
In the VM:
public MyStatus Status
{
get { return _status; }
set
{
_status = value;
OnPropertyChanged("Status");
ButtonBg = Colors.Red;
}
}
public Color ButtonBg
{
get { ... }
set { ... }
}
Since your button background is bound to a property on your view model, then you have the freedom to change that in reaction to whatever is going on in your view model without needing to move logic or code out to converters and templates.
Personally, I have NOT been able to use custom UserControls with MVVM. Either my mind hasn't wrapped around how to use them together or they just don't mix. I use DataTemplates for everything that's not a Window.
Keeping it concise...
OnOffStatusVM : INPC
string Status
Color Color (or Brush)
(set Color when enum value updates)
(OnOffStatus DataTemplate)
<DataTemplate DataType="{x:Type ViewModel:OnOffStatusVM}" x:Shared="False" x:Key="rezOnOffStatus">
<Grid>
<Label Height="Auto" HorizontalAlignment="Left" Content="{Binding Status}" Width="280" />
<Button Style="{StaticResource GlassButton}" Height="14" Width="14" Background="{Binding Color}" Grid.Column="1" />
</Grid>
</DataTemplate>
Usage if DataContext derives from OnOffStatusVM
<ContentPresenter Content="{Binding}" ContentTemplate="{StaticResource rezOnOffStatus}" />
Usage if DataContext has a OnOffStatusVM OnOffStatus property
<ContentPresenter Content="{Binding OnOffStatus}" ContentTemplate="{StaticResource rezOnOffStatus}" />
Clarification provided if needed..

How to Display ObservableCollection<string> in a UserControl

I'm new to WPF and I've found some similar questions but can't quite figure out the last part. I have a ViewModel with an ObservableCollection that contains error messages. I want to display these on the form AND allow the user to select and copy all or part of the messages. (In the past in WinForm apps I used a RichTextBox for this, but I can't figure out how to bind to one to the collection in WPF.)
I got the look I was after with the following xaml, but there is no built-in way to select and copy like I could with a RichTextBox. Does anyone know which control I should use or if there is way to enable selecting/copying the contents of all the TextBlocks, or a way to bind this to a RichTextBox?
<Grid Margin="6">
<ScrollViewer VerticalScrollBarVisibility="Auto" Height="40" Grid.Column="0" Margin="6">
<ItemsControl ItemsSource="{Binding ErrorMessages}" >
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Mode=OneWay}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
[Edit]
#Andrey Shvydky - This wouldn't fit in a comment.
It took me a while to figure out the proper syntax (especially the /, thing) but eventually I ended up with the Flow Document syntax shown below. It looks correct on the form and at first seems to support select all/copy. But when I paste after a select all/copy nothing ever shows up. Anyone know why?
<Grid Margin="6">
<FlowDocumentScrollViewer>
<FlowDocument >
<Paragraph>
<ItemsControl ItemsSource="{Binding ErrorMessages, Mode=OneWay}" />
<Run Text="{Binding /, Mode=OneWay}" />
</Paragraph>
</FlowDocument>
</FlowDocumentScrollViewer>
</Grid>
Unless you have a great amount of messages a simple converter might be viable:
<TextBox IsReadOnly="True">
<TextBox.Text>
<Binding Path="Messages" Mode="OneWay">
<Binding.Converter>
<vc:JoinStringsConverter />
</Binding.Converter>
</Binding>
</TextBox.Text>
</TextBox>
public class JoinStringsConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var strings = value as IEnumerable<string>;
return string.Join(Environment.NewLine, strings);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
May be usefull to generate FlowDocument and show this document in FlowDocumentReader.
Try to start from this article: Flow Document Overview.
Example of generation:
void ShowErrors(FlowDocumentReader reader, Exception[] errors) {
FlowDocument doc = new FlowDocument();
foreach (var e in errors) {
doc.Blocks.Add(new Paragraph(new Run(e.GetType().Name)) {
Style = (Style)this.FindResource("header")
});
doc.Blocks.Add(new Paragraph(new Run(e.Message)) {
Style = (Style)this.FindResource("text")
});
}
reader.Document = doc;
}
In this example I have added some styles for text in flowdocument. PLease look at XAML:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Style x:Key="header" TargetType="{x:Type Paragraph}">
<Setter Property="FontWeight" Value="Bold"/>
</Style>
<Style x:Key="text" TargetType="{x:Type Paragraph}">
<Setter Property="Margin" Value="30, 0, 0, 0"/>
</Style>
</Window.Resources>
<FlowDocumentReader Name="reader">
</FlowDocumentReader>
Result:
Simplest way:
Assuming your viewmodel implements INotifyPropertyChange, create an event handler for the ObservableCollection PropertyChanged event. Create a property which aggregates all of the items in the observable colleciton into a single string. Whenever the observable collection changes, fire off a notification event for your new property. Bind to that property
public class ViewModel : INotifyPropertyChange
{
public ViewModel()
{
MyStrings.CollectionChanged += ChangedCollection;
}
public ObservableCollection<string> MyStrings{get;set;}
public void ChangedCollection(args,args)
{
base.PropertyChanged("MyAllerts");
}
public string MyAllerts
{
get
{
string collated = "";
foreach(var allert in MyStrings)
{
collated += allert;
collated += "\n";
}
}
}
}
I know this code is fraught with errors (i wrote it in SO instead of VS), but it should give you some idea.
<Grid Margin="6">
<ScrollViewer VerticalScrollBarVisibility="Auto" Height="40" Grid.Column="0" Margin="6">
<ItemsControl ItemsSource="{Binding ErrorMessages}" >
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding ViewModelMemberRepresentingYourErrorMessage}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>

MVVM Light Commands within an ItemsControl

I'm just trying my hand at WP7 dev using the MVVM Light framework.
I'm trying to fire a button command inside an ItemsControl, essentialy it's a list of cars and I'd like each element to have an edit button.
The Relevant piece of the View:
<ItemsControl ItemsSource="{Binding MyCars}" >
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid x:Name="CarViewGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="100" />
<ColumnDefinition Width="Auto" MinWidth="302"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" MinHeight="40" />
<RowDefinition Height="Auto" MinHeight="32" />
<RowDefinition Height="Auto" MinHeight="32" />
<RowDefinition Height="Auto" MinHeight="32" />
</Grid.RowDefinitions>
<TextBlock x:Name="CarName" Text="{Binding Name, Mode=TwoWay}" Margin="7,0" Grid.Row="0" Grid.ColumnSpan="2" FontSize="32" FontWeight="Bold" FontStyle="Normal" />
<TextBlock x:Name="Make" Text="{Binding Make, Mode=TwoWay}" Margin="15,0" Grid.Row="1" Grid.Column="0" FontSize="24" />
<TextBlock x:Name="Model" Text="{Binding Model, Mode=TwoWay}" Grid.Row="1" Grid.Column="1" FontSize="24" />
<TextBlock x:Name="Odometer" Text="{Binding Odometer, Mode=TwoWay}" Margin="15,0" Grid.Row="2" Grid.ColumnSpan="2" FontSize="24" />
<Button x:Name="EditCarButton" Content="Edit" Grid.Row="3" Grid.Column="1" HorizontalAlignment="Right" Width="100" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<cmd:EventToCommand Command="{Binding EditCar}" CommandParameter="{Binding}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
My ViewModel contains this:
public RelayCommand OpenNewForm { get; private set; }
public CarViewModel()
{
//Snip
EditCar = new RelayCommand<Car>(c =>
{
CurrentCar = c;
FormVisible = true;
});
}
Now as you can see I'm trying to pass the current Car object that is bound through the CommandParameter. My delegate never fires so I'm guessing I've got something wrong in my binding regarding the current DataContext.
Anybody got any ideas as to what I'm doing wrong?
In a DataTemplate, the DataContext is set by default to the item that is represented by the DataTemplate (in that case, the Car object). If the EditCar command is on the main viewmodel (which also contains the MyCars collection), you need to explicitly set the Source of the Binding to that object. This would be (assuming that you are using the MVVM Light's ViewModelLocator and that your VM is named Main) {Binding Source={StaticResource Locator}, Path=Main.EditCar}
Cheers,
Laurent
Its going to fire EditCar on a car item. There are a couple ways to solve this, since you're using mvvm light try.
Appologies to Laurent. I posted the wrong link. My intention was that since the original poster was using MVVM Light that Dan Wahlin's DataContextProxy or a RelativeSource binding solution would work. I was going to go on and explain how if using CM an event from a child item could bubble up but I didn't. The link to CM dotnetrocks was something I pasted previously.
I have found that its alot easier to make my collections VM collections instead of Entitycollections. I used to use entitycollections and then I started running into those problems like you are describing. But Now each VM in the Collection is 'selfaware' and can act on itself without jumping through major hoops.
You would have the button that you are clicking as part of the CarsVM and it would have access to all the properties of the carVM which would have access to all the properties of your Car Entity.
Sample from My App:
public partial class ReadmitPatientListViewModel : ViewModelBase
{
/// <summary>
/// Initializes a new instance of the ReadmitPatientListViewModel class.
/// </summary>
////public override void Cleanup()
////{
//// // Clean own resources if needed
//// base.Cleanup();
////}
#region Declarations
ICommand _openSurveyCommand;
Messenger _messenger = Messenger.Default;
#endregion
#region Command Properties
public ICommand OpenSurveyCommand
{
get
{
if (_openSurveyCommand == null)
{
_openSurveyCommand = new RelayCommand(() => OnSurveyCommandExecute());
}
return _openSurveyCommand;
}
private set { }
}
#endregion
#region Command Methods
private void OnSurveyCommandExecute()
{
Wait.Begin("Loading Patient List...");
_messenger.Send<ReadmitPatientListViewModel>(this);
_messenger.Send<Messages.NavigationRequest<SubClasses.URI.PageURI>>(GetNavRequest_QUESTIONAIRRESHELL());
}
#endregion
#region Properties
#endregion
private static Messages.NavigationRequest<SubClasses.URI.PageURI> GetNavRequest_QUESTIONAIRRESHELL()
{
Messages.NavigationRequest<SubClasses.URI.PageURI> navRequest =
new Messages.NavigationRequest<SubClasses.URI.PageURI>(
new SubClasses.URI.PageURI(Helpers.PageLinks.QUESTIONAIRRESHELL, System.UriKind.Relative));
return navRequest;
}
partial void OnCreated()
{
}
}
These are the properties in the primary vm that my Expander binds to:
public CollectionViewSource SearchResultsCVS { get; private set; }
public ICollection<ViewModel.ReadmitPatientListViewModel> SearchResults { get; private set; }
The collection is the soure for the CVS.....when the completeSurveyButton is clicked a navigation request is sent,and a copy of the viewmodel is sent to any listeners to manipulate.

How do I expand a control in WPF to make room for error messages in an ErrorTemplate?

I have a WPF window that uses validation. I created an error template that puts a red border around an element that fails validation and displays the error message below. This works fine, but the error message is rendered on top of any controls beneath the control with the error. The best I can tell, this happens because the error template renders on the Adorner Layer, which is on top of everything else. What I'd like to have happen is for everything else to move down to make room for the error message. Is there a way to do this? All of the examples on the web seem to use a tool tip and use a simple indicator like an asterisk or exclamation point that doesn't use much room.
Here is the template:
<ControlTemplate x:Key="ValidationErrorTemplate">
<StackPanel>
<Border BorderBrush="Red" BorderThickness="2" CornerRadius="2">
<AdornedElementPlaceholder x:Name="placeholder"/>
</Border>
<TextBlock Foreground="Red" FontSize="10" Text="{Binding ElementName=placeholder, Path=AdornedElement.(Validation.Errors)[0].ErrorContent, FallbackValue=Error!}"></TextBlock>
</StackPanel>
</ControlTemplate>
Here are the controls using the template (I typed some of this out, so ignore any syntax errors):
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBox Name="Account" Grid.Row="0" Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}" Width="200">
<TextBox.Text>
<Binding Path="AccountNumber">
<Binding.ValidationRules>
<validators:RequiredValueValidationRule/>
<validators:NumericValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<TextBox Name="Expiration" Grid.Row="1" Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}" Width="100" Margin="0,2,5,2">
<TextBox.Text>
<Binding Path="ExpirationDate">
<Binding.ValidationRules>
<validators:ExpirationDateValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</Grid>
EDIT: Alright, I'm not positive that this is the best solution (I sure hope someone can provide a better one), but here it goes:
Instead of using the Validation.ErrorTeplate, which will present all of the visuals in the AdornerLayer, you can add some TextBlocks and bind them to Validation.HasError and (Validation.Errors)[0].ErrorContent, using a customer IValueConverter to convert the Validation.HasError bool to a Visibility value. It would look something like the following:
Window1.cs:
<Window x:Class="WpfApplicationTest.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:local="clr-namespace:WpfApplicationTest"
Title="Window1" Height="300" Width="300">
<Grid Margin="10">
<Grid.Resources>
<!-- The person we are binding to -->
<local:Person x:Key="charles" Name="Charles" Age="20" />
<!-- The convert to use-->
<local:HasErrorToVisibilityConverter x:Key="visibilityConverter" />
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- The name -->
<TextBox Name="NameTextBox" Grid.Row="0" Text="{Binding Source={StaticResource charles}, Path=Name, ValidatesOnDataErrors=true}" />
<TextBlock Grid.Row="1"
Foreground="Red"
Text="{Binding ElementName=NameTextBox, Path=(Validation.Errors)[0].ErrorContent}"
Visibility="{Binding ElementName=NameTextBox, Path=(Validation.HasError), Converter={StaticResource visibilityConverter}}" />
<!-- The age -->
<TextBox Name="AgeTextBox" Grid.Row="2" Text="{Binding Source={StaticResource charles}, Path=Age, ValidatesOnExceptions=true}" />
<TextBlock Grid.Row="3"
Foreground="Red"
Text="{Binding ElementName=AgeTextBox, Path=(Validation.Errors)[0].ErrorContent}"
Visibility="{Binding ElementName=AgeTextBox, Path=(Validation.HasError), Converter={StaticResource visibilityConverter}}" />
</Grid>
</Window>
Person.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Text.RegularExpressions;
namespace WpfApplicationTest
{
public class Person : IDataErrorInfo
{
public string Name { get; set; }
public int Age { get; set; }
#region IDataErrorInfo Members
string IDataErrorInfo.Error
{
get { throw new NotImplementedException(); }
}
string IDataErrorInfo.this[string columnName]
{
get
{
switch (columnName)
{
case ("Name"):
if (Regex.IsMatch(this.Name, "[^a-zA-Z ]"))
{
return "Name may contain only letters and spaces.";
}
else
{
return null;
}
default:
return null;
}
}
}
#endregion
}
}
HasErrorToVisibilityConverter.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Data;
using System.Windows;
namespace WpfApplicationTest
{
[ValueConversion(typeof(bool), typeof(Visibility))]
public class HasErrorToVisibilityConverter : IValueConverter
{
#region IValueConverter Members
object IValueConverter.Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
bool hasError = (bool)value;
return hasError ? Visibility.Visible : Visibility.Collapsed;
}
object IValueConverter.ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
}
It doesn't scale as well as having a single ControlTemplate that you can reference in all of your controls, but it's the only solution I've found. I feel your pain - just about every example I can find on the topic of WPF validation is very simple, and almost always uses '!' or '*' preceding the control, with a tooltip bound to (Validation.Errors)[0].ErrorContent...
Best of luck to ya! If I find a better solution, I'll update this ;)

How do I make a WPF data template fill the entire width of the listbox?

I have a ListBox DataTemplate in WPF. I want one item to be tight against the left side of the ListBox and another item to be tight against the right side, but I can't figure out how to do this.
So far I have a Grid with three columns, the left and right ones have content and the center is a placeholder with it's width set to "*". Where am I going wrong?
Here is the code:
<DataTemplate x:Key="SmallCustomerListItem">
<Grid HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="*"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<WrapPanel HorizontalAlignment="Stretch" Margin="0">
<!--Some content here-->
<TextBlock Text="{Binding Path=LastName}" TextWrapping="Wrap" FontSize="24"/>
<TextBlock Text=", " TextWrapping="Wrap" FontSize="24"/>
<TextBlock Text="{Binding Path=FirstName}" TextWrapping="Wrap" FontSize="24"/>
</WrapPanel>
<ListBox ItemsSource="{Binding Path=PhoneNumbers}" Grid.Column="2" d:DesignWidth="100" d:DesignHeight="50"
Margin="8,0" Background="Transparent" BorderBrush="Transparent" IsHitTestVisible="False" HorizontalAlignment="Stretch"/>
</Grid>
</DataTemplate>
I also had to set:
HorizontalContentAlignment="Stretch"
on the containing ListBox.
<Grid.Width>
<Binding Path="ActualWidth"
RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type ScrollContentPresenter}}" />
</Grid.Width>
Ok, here's what you have:
Column 0: WrapPanel
Column 1: Nothing
Column 2: ListBox
It sounds like you want WrapPanel on the left edge, ListBox on the right edge, and space to take up what's left in the middle.
Easiest way to do this is actually to use a DockPanel, not a Grid.
<DockPanel>
<WrapPanel DockPanel.Dock="Left"></WrapPanel>
<ListBox DockPanel.Dock="Right"></ListBox>
</DockPanel>
This should leave empty space between the WrapPanel and the ListBox.
Extending Taeke's answer, setting the ScrollViewer.HorizontalScrollBarVisibility="Hidden" for a ListBox allows the child control to take the parent's width and not have the scroll bar show up.
<ListBox Width="100" ScrollViewer.HorizontalScrollBarVisibility="Hidden">
<Label Content="{Binding Path=., Mode=OneWay}" HorizontalContentAlignment="Stretch" Height="30" Margin="-4,0,0,0" BorderThickness="0.5" BorderBrush="Black" FontFamily="Calibri" >
<Label.Width>
<Binding Path="Width" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBox}}" />
</Label.Width>
</Label>
</ListBox >
The Grid should by default take up the whole width of the ListBox because the default ItemsPanel for it is a VirtualizingStackPanel. I'm assuming that you have not changed ListBox.ItemsPanel.
Perhaps if you got rid of the middle ColumnDefinition (the others are default "*"), and put HorizontalAlignment="Left" on your WrapPanel and HorizontalAlignment="Right" on the ListBox for phone numbers. You may have to alter that ListBox a bit to get the phone numbers even more right-aligned, such as creating a DataTemplate for them.
If you want to use a Grid, then you need to change your ColumnDefinitions to be:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
If you don't need to use a Grid, then you could use a DockPanel:
<DockPanel>
<WrapPanel DockPanel.Dock="Left">
<!--Some content here-->
<TextBlock Text="{Binding Path=LastName}" TextWrapping="Wrap" FontSize="24"/>
<TextBlock Text=", " TextWrapping="Wrap" FontSize="24"/>
<TextBlock Text="{Binding Path=FirstName}" TextWrapping="Wrap" FontSize="24"/>
</WrapPanel>
<ListBox DockPanel.Dock="Right" ItemsSource="{Binding Path=PhoneNumbers}"
Margin="8,0" Background="Transparent" BorderBrush="Transparent" IsHitTestVisible="False"/>
<TextBlock />
</DockPanel>
Notice the TextBlock at the end. Any control with no "DockPanel.Dock" defined will fill the remaining space.
Taeke's answer works well, and as per vancutterromney's answer you can disable the horizontal scrollbar to get rid of the annoying size mismatch. However, if you do want the best of both worlds--to remove the scrollbar when it is not needed, but have it automatically enabled when the ListBox becomes too small, you can use the following converter:
/// <summary>
/// Value converter that adjusts the value of a double according to min and max limiting values, as well as an offset. These values are set by object configuration, handled in XAML resource definition.
/// </summary>
[ValueConversion(typeof(double), typeof(double))]
public sealed class DoubleLimiterConverter : IValueConverter
{
/// <summary>
/// Minimum value, if set. If not set, there is no minimum limit.
/// </summary>
public double? Min { get; set; }
/// <summary>
/// Maximum value, if set. If not set, there is no minimum limit.
/// </summary>
public double? Max { get; set; }
/// <summary>
/// Offset value to be applied after the limiting is done.
/// </summary>
public double Offset { get; set; }
public static double _defaultFailureValue = 0;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null || !(value is double))
return _defaultFailureValue;
double dValue = (double)value;
double minimum = Min.HasValue ? Min.Value : double.NegativeInfinity;
double maximum = Max.HasValue ? Max.Value : double.PositiveInfinity;
double retVal = dValue.LimitToRange(minimum, maximum) + Offset;
return retVal;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Then define it in XAML according to the desired max/min values, as well an offset to deal with that annoying 2-pixel size mismatch as mentioned in the other answers:
<ListBox.Resources>
<con:DoubleLimiterConverter x:Key="conDoubleLimiter" Min="450" Offset="-2"/>
</ListBox.Resources>
Then use the converter in the Width binding:
<Grid.Width>
<Binding Path="ActualWidth" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type ScrollContentPresenter}}" Converter="{StaticResource conDoubleLimiter}" />
</Grid.Width>
The method in Taeke's answer forces a horizontal scroll bar. This can be fixed by adding a converter to reduce the grid's width by the width of the vertical scrollbar control.
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
namespace Converters
{
public class ListBoxItemWidthConverter : MarkupExtension, IValueConverter
{
private static ListBoxItemWidthConverter _instance;
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return System.Convert.ToInt32(value) - SystemParameters.VerticalScrollBarWidth;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
public override object ProvideValue(IServiceProvider serviceProvider)
{
return _instance ?? (_instance = new ListBoxItemWidthConverter());
}
}
}
Add a namespace to the root node of your XAML.
xmlns:converters="clr-namespace:Converters"
And update the Grid width to use the converter.
<Grid.Width>
<Binding Path="ActualWidth" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type ScrollContentPresenter}}" Converter="{converters:ListBoxItemWidthConverter}"/>
</Grid.Width>

Resources