How to use multibinding on a rectangle-command in mvvm? - wpf

I got the following rectangle
<Rectangle
Width="{Binding Width}"
Height="{Binding Length}"
Tag="{Binding Id}"
Name="rectangleDrawnMachine">
<i:Interaction.Triggers>
<i:EventTrigger
EventName="MouseDown">
<cmd:EventToCommand
Command="{Binding Main.UpdateSelectedMachine, Mode=OneWay, Source={StaticResource Locator}}"
PassEventArgsToCommand="True"
CommandParameter="{Binding ElementName=rectangleDrawnMachine}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Rectangle>
The rectangle is bound to a model which is declared in an above ItemsControl. The document-structure is like the following:
<Grid>
<ItemsControl ItemsSource="{Binding AllMachines}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Name="canvasDrawnMachines" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Rectangle Name="rectangleDrawnMachine"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
Now my UpdateSelectedMachine-command needs at least three properties of the rectangle:
Position x
Position y
ID / Tag
With the CommandParameter of the rectangle itself my command will get a lot of informations about the rectangle (like the neccessary tag). But it doesnt get the neccessary information about the (X- & Y-)position of the canvas.
So my question is: how to use multibinding on my rectangle-command? And how to transfer the positions of the canvas?

You cannot pass multiple values by using command parameter.
In order to do so, you have to use multi binding.
<cmd:EventToCommand
Command="{Binding Main.UpdateSelectedMachine, Mode=OneWay, Source={StaticResource Locator}}"
PassEventArgsToCommand="True">
<cmd:EventToCommand.CommandParameter>
<MultiBinding Converter="{StaticResource YourConverter}">
<Binding Path="Canvas.Left" ElementName="canvasDrawnMachines"/>
<Binding Path="Canvas.Top" ElementName="canvasDrawnMachines"/>
<Binding Path="Tag" ElementName="canvasDrawnMachines"/>
</MultiBinding>
</cmd:EventToCommand.CommandParameter>
Your converter:
public class YourConverter : IMultiValueConverter
{
public object Convert(object[] values, ...)
{
return values.Clone();
}
}
Then, execution command logic:
public void OnExecute(object parameter)
{
var values = (object[])parameter;
var left = (double)values[0];
var top = (double)values[1];
var tag = values[2];
}

You can get the values of the Canvas.Left and Canvas.Top attached properties of the Rectangle that you are passing as the command parameter to the command like this:
double x = Canvas.GetLeft(rectangle);
double y = Canvas.GetTop(rectangle);
Do you know how to get the position in XAML-way?
Use a MultiBinding with a converter and bind to the Canvas.Left and Canvas.Top properties:
<MultiBinding Converter="{StaticResource converter}">
<Binding Path="(Canvas.Left)" ElementName="canvasDrawnMachines"/>
<Binding Path="(Canvas.Top)" ElementName="canvasDrawnMachines"/>
<Binding Path="Tag" ElementName="canvasDrawnMachines"/>
</MultiBinding>

Related

Unable to center a canva inside a canva

I've a view in WPF where I need to center a canvas within a canvas.
I know it's not the most suitable container for this, but we have other components that will come in this canvas where it will simplify a lot our job.
So basically, I've currently this code:
<Canvas Name="RootContainer" Background="#373B3F" ClipToBounds="True" MouseLeftButtonDown="OnMainCanvasMouseLeftButtonDown">
<Canvas.DataContext>
<local:SomeViewModel/>
</Canvas.DataContext>
<Canvas Name="SomeContainer" Background="#373B3F" MouseMove="OnCanvasMouseMove" MouseWheel="OnCanvasMouseWheel">
<Canvas.Left>
<MultiBinding Converter="{StaticResource CenterValueConverter}">
<Binding ElementName="RootContainer" Path="ActualWidth" />
<Binding ElementName="SomeContainer" Path="ActualWidth" />
</MultiBinding>
</Canvas.Left>
<Canvas.Top>
<MultiBinding Converter="{StaticResource CenterValueConverter}">
<Binding ElementName="RootContainer" Path="ActualHeight" />
<Binding ElementName="SomeContainer" Path="ActualHeight" />
</MultiBinding>
</Canvas.Top>
<ItemsControl ItemsSource="{Binding SomeChilds}" Name="ItemsControl">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding Left}" />
<Setter Property="Canvas.Top" Value="{Binding Top}" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Content="{Binding}" ContentTemplateSelector="{StaticResource ConventionBasedDataTemplateSelector}"
MouseLeftButtonDown="OnMouseLeftButtonDown" PreviewMouseLeftButtonUp="OnPreviewMouseLeftButtonUp" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Some other controls over here -->
</Canvas>
</Canvas>
And this converter:
public class CenterValueConverter : IMultiValueConverter
{
public object Convert(object[] values,
Type targetType,
object parameter,
CultureInfo culture)
{
if (values == null || values.Length != 2)
{
return null;
}
double totalWidth = (double)values[0];
double size = (double)values[1];
return totalWidth / 2 - (size / 2);
}
public object[] ConvertBack(object value,
Type[] targetTypes,
object parameter,
CultureInfo culture)
{
throw new NotSupportedException();
}
}
The issue is that the second value(SomeContainer.ActualWidth/ActualHeight) always comes with a value of 0(even when I've some real elements it).
Any idea why and how to fix this? Or another XAML way of centering SomeContainer inside of the RootContainer ?
EDIT
Maybe some additional informations on why I planned to use so much Canvas.
The RootContainer one is because SomeContainer will have transformation(scale for zooming) and translation for panning
The SomeContainer could be something different I guess
The Canvas inside the ItemControls is because each elements will be positioned as a very specific place.
I guess your ActualHeight/ActualWidth are = 0 because you didn't load the window yet.
To make these calculous taking in account dimensions of Canvas you may use 'OnContentRendered' event, that will be launched after the window is displayed, so you will have their dimensions appearing. If your Canvas may change dimensions, you may also use SizeChanged event.
Even I think it is a bit complicated, and you could just use that XAML code taken from Clemen's answer :
<Canvas Background="Transparent"
SizeChanged="ViewportSizeChanged"
MouseLeftButtonDown="ViewportMouseLeftButtonDown"
MouseLeftButtonUp="ViewportMouseLeftButtonUp"
MouseMove="ViewportMouseMove"
MouseWheel="ViewportMouseWheel">
<Canvas x:Name="canvas" Width="1000" Height="600">
<Canvas.RenderTransform>
<MatrixTransform x:Name="transform"/>
</Canvas.RenderTransform>
<Ellipse Fill="Red" Width="100" Height="100" Canvas.Left="100" Canvas.Top="100"/>
<Ellipse Fill="Green" Width="100" Height="100" Canvas.Right="100" Canvas.Bottom="100"/>
</Canvas>
</Canvas>

How to clear a TextBox in MVVM?

I have a TextBox in a DataTemplate declared as follows:
<TextBox Grid.Row="1" Grid.Column="1" Margin="0,4,0,0">
<i:Interaction.Triggers>
<i:EventTrigger EventName="LostFocus">
<cmd:EventToCommand Command="{Binding DataContext.NotesEnteredCommand,
RelativeSource={RelativeSource FindAncestor, AncestorType=Window}}">
<cmd:EventToCommand.CommandParameter>
<MultiBinding Converter="{StaticResource SimpleMultiValueConverter}">
<Binding Path="Row.OID" />
<Binding Path="Text" RelativeSource="{RelativeSource FindAncestor, AncestorType=TextBox}" />
</MultiBinding>
</cmd:EventToCommand.CommandParameter>
</cmd:EventToCommand>
</i:EventTrigger>
</i:Interaction.Triggers>
<TextBox.InputBindings>
<KeyBinding Key="Enter" Command="{Binding DataContext.NotesEnteredCommand, RelativeSource={RelativeSource FindAncestor, AncestorType=Window}}">
<KeyBinding.CommandParameter>
<MultiBinding Converter="{StaticResource SimpleMultiValueConverter}">
<Binding Path="Row.OID" />
<Binding Path="Text" RelativeSource="{RelativeSource FindAncestor, AncestorType=TextBox}" />
</MultiBinding>
</KeyBinding.CommandParameter>
</KeyBinding>
</TextBox.InputBindings>
What this TextBox basically does is execute a MVVM-Light RelayCommand when the Enter key is pressed or when losing focus.
My problem is that I cannot figure out a way in MVVM to clear the TextBox's Text value through XAML in the above two scenarios. It's very easy with in code-behind, but I can't figure it out in MVVM.
Any ideas?
If the text is part of your data layer and application logic, a string should exist in your Model or ViewModel and be cleared from there
For example,
<TextBox Text="{Binding NewNote}" ... />
and
void NotesEntered(int oid)
{
SaveNewNote(oid);
NewNote = string.Empty;
}
If it's part of the UI layer only, it should just be cleared with code-behind. It's perfectly acceptable to have UI-specific logic in the code-behind the UI, as that still maintains the separation of layers.
NewNoteTextBox_LostFocus(object sender, EventArgs e)
{
(sender as TextBox).Text = string.Empty;
}
NewNoteTextBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Keys.Enter)
(sender as TextBox).Text = string.Empty;
}
You can use a UpdateSourceHelper. This really helped me out calling an event with no code-behind.
See here http://www.wiredprairie.us/blog/index.php/archives/1701
All you have to do is create a class "UpdateSourceHelper", connect it with your xaml like this
xmlns:local="using:WiredPrairie.Converter
and bind it to your TextBox (or whatever you want to bind to)...
<TextBox Height="Auto" Margin="0,6" Grid.Row="1" TextWrapping="Wrap" TabIndex="0"
Text="{Binding Value}"
local:UpdateSourceHelper.IsEnabled="True"
local:UpdateSourceHelper.UpdateSourceText="{Binding Value, Mode=TwoWay}"/>
If you want call LostFocus Event in the Helper, you simply have to add these 2 lines in your Helper:
tb.LostFocus += AttachedTextBoxLostFocus;
tb.LostFocus -= AttachedTextBoxLostFocus;
So it would look like this:
TextBox tb = (TextBox)obj;
if ((bool)args.NewValue)
{
tb.LostFocus += AttachedTextBoxLostFocus;
}
else
{
tb.LostFocus -= AttachedTextBoxLostFocus;
}
Right click on AttachedTextBoxLostFocus and generate the method. Now you can handle the Event like a code-behind event.

wpf combobox item template for the selected value and search functionality

I'm using a combobox in my application and I am populating it with classes something like this:
namespace Foo.Bar{
public class Item
{
public string lastName;
public string firstName;
public Foo theMeatyPart;
}
}
I can populate the dropdown with "lastName, firstName" using an itemTamplate but then the selected value shows up as "Foo.Bar.Item". How can I apply the same template to the selectedItem and also, have the search functionality work without overrriding the ToString method of Item?
Here is the xaml:
<Style x:Key="SearchComboStyle" TargetType="ComboBox">
<Style.Setters>
<Setter Property="Width" Value="150"></Setter>
</Style.Setters>
</Style>
<DataTemplate x:Key="SearchComboItemTemplate" >
<TextBlock DataContext="{Binding}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}, {1}">
<Binding Path="lastName"/>
<Binding Path="firstName"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
<ComboBox ItemTemplate="{StaticResource SearchComboItemTemplate}" Style="{StaticResource SearchComboStyle}"
ItemsSource="{Binding Path=PhysiciansList, RelativeSource={RelativeSource AncestorType=local:ExamViewerControl, AncestorLevel=1}}" IsTextSearchEnabled="True" IsTextSearchCaseSensitive="False" IsEditable="True" TextSearch.TextPath="Person.LastName" />
UPD: Looks like you need to set SelectionBoxItemTemplate.
You can use DisplayMemberPath or TextSearch.TextPath to enable search without modifying ToString().

Use different template for last item in a WPF itemscontrol

I'm using a custom template in my itemscontrol to display the following result:
item 1, item 2, item3,
I want to change the template of the last item so the result becomes:
item 1, item2, item3
The ItemsControl:
<ItemsControl ItemsSource="{Binding Path=MyCollection}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Name}"/>
<TextBlock Text=", "/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Is there anyone who can give a solution for my problem? Thank you!
I've found the solution for my problem using only XAML. If there is anybody who needs to do the same, use this:
<ItemsControl ItemsSource="{Binding Path=MyCollection}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock x:Name="comma" Text=", "/>
<TextBlock Text="{Binding}"/>
</StackPanel>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource PreviousData}}" Value="{x:Null}">
<Setter TargetName="comma" Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
You can use DataTemplateSelector, in SelectTemplate() method you can check whether item is the last and then return an other template.
In XAML:
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentPresenter
ContentTemplateSelector = "{StaticResource MyTemplateSelector}">
In Code behind:
private sealed class MyTemplateSelector: DataTemplateSelector
{
public override DataTemplate SelectTemplate(
object item,
DependencyObject container)
{
// ...
}
}
This solution affects the last row and updates with changes to the underlying collection:
CodeBehind
The converter requires 3 parameters to function properly - the current item, the itemscontrol, the itemscount, and returns true if current item is also last item:
class LastItemConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
int count = (int)values[2];
if (values != null && values.Length == 3 && count>0)
{
System.Windows.Controls.ItemsControl itemsControl = values[0] as System.Windows.Controls.ItemsControl;
var itemContext = (values[1] as System.Windows.Controls.ContentPresenter).DataContext;
var lastItem = itemsControl.Items[count-1];
return Equals(lastItem, itemContext);
}
return DependencyProperty.UnsetValue;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
XAML
The Data-Trigger for a DataTemplate, that includes a textbox named 'PART_TextBox':
<DataTemplate.Triggers>
<DataTrigger Value="True" >
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource LastItemConverter}">
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}" />
<Binding RelativeSource="{RelativeSource Self}"/>
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}" Path="Items.Count"/>
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Foreground" TargetName="PART_TextBox" Value="Red" />
</DataTrigger>
</DataTemplate.Triggers>
The converter as a static resource in the Xaml
<Window.Resources>
<local:LastItemConverter x:Key="LastItemConverter" />
</Window.Resources>
SnapShot
And a snapshot of it in action
The code has been added to the itemscontrol from this 'codeproject'
https://www.codeproject.com/Articles/242628/A-Simple-Cross-Button-for-WPF
Note the last item's text in red
One question... I see you're using an ItemsControl as opposed to say a ListBox and that it appears to be bound to a collection of strings, and that you're only trying to display the resulting text without formatting the individual parts, which makes me wonder if your desired output is actually the string itself as mentioned in the question, and not an actual ItemsControl per se.
If I'm correct about that, have you considered just using a simple TextBlock bound to the items collection, but fed through a converter? Then Inside the converter, you would cast value to an array of strings, then in the Convert method, simply Join them using a comma as the separator which will automatically, only add them between elements, like so...
var strings = (IEnumerable<String>)value;
return String.Join(", ", strings);

Can I do Text search with multibinding

I have below combo box in mvvm-wpf application. I need to implement "Text search" in this..(along with multibinding). Can anybody help me please.
<StackPanel Orientation="Horizontal">
<TextBlock Text="Bid Service Cat ID"
Margin="2"></TextBlock>
<ComboBox Width="200"
Height="20"
SelectedValuePath="BidServiceCategoryId"
SelectedValue="{Binding RelativeSource={RelativeSource AncestorType={x:Type UserControl}},
Path=DataContext.SelectedBidServiceCategoryId.Value}"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type UserControl}},
Path=DataContext.BenefitCategoryList}"
Margin="12,0">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock DataContext="{Binding}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}: {1}">
<Binding Path="BidServiceCategoryId" />
<Binding Path="BidServiceCategoryName" />
</MultiBinding>
</TextBlock.Text></TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
Unfortunately, TextSearch.Text doesn't work in a DataTemplate. Otherwise you could have done something like this
<ComboBox ...>
<ComboBox.ItemContainerStyle>
<Style TargetType="{x:Type ComboBoxItem}">
<Setter Property="TextSearch.Text">
<Setter.Value>
<MultiBinding StringFormat="{}{0}: {1}">
<Binding Path="BidServiceCategoryId"/>
<Binding Path="BidServiceCategoryName"/>
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
However this won't work, so I see two solutions to your problem.
First way
You set IsTextSearchEnabled to True for the ComboBox, override ToString in your source class and change the MultiBinding in the TextBlock to a Binding
Xaml
<ComboBox ...
IsTextSearchEnabled="True">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
Source class
public class TheNameOfYourSourceClass
{
public override string ToString()
{
return String.Format("{0}: {1}", BidServiceCategoryId, BidServiceCategoryName);
}
//...
}
Second Way
If you don't want to override ToString I think you'll have to introduce a new Property in your source class where you combine BidServiceCategoryId and BidServiceCategoryName for the TextSearch.TextPath. In this example I call it BidServiceCategory. For this to work, you'll have to call OnPropertyChanged("BidServiceCategory"); when BidServiceCategoryId or BidServiceCategoryName changes as well. If they are normal CLR properties, you can do this in set, and if they are dependency properties you'll have to use the property changed callback
Xaml
<ComboBox ...
TextSearch.TextPath="BidServiceCategory"
IsTextSearchEnabled="True">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock DataContext="{Binding}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}: {1}">
<Binding Path="BidServiceCategoryId" />
<Binding Path="BidServiceCategoryName" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
Source class
public class TheNameOfYourSourceClass
{
public string BidServiceCategory
{
get
{
return String.Format("{0}: {1}", BidServiceCategoryId, BidServiceCategoryName);
}
}
private string m_bidServiceCategoryId;
public string BidServiceCategoryId
{
get
{
return m_bidServiceCategoryId;
}
set
{
m_bidServiceCategoryId = value;
OnPropertyChanged("BidServiceCategoryId");
OnPropertyChanged("BidServiceCategory");
}
}
private string m_bidServiceCategoryName;
public string BidServiceCategoryName
{
get
{
return m_bidServiceCategoryName;
}
set
{
m_bidServiceCategoryName = value;
OnPropertyChanged("BidServiceCategoryName");
OnPropertyChanged("BidServiceCategory");
}
}
}
I don't know if your text search has to search ALL the text, but if you want to search from the category ID, you can just set the TextSearch.TextPath property to BidServiceCategoryId. That should also be helpful for anyone who wants to use multibinding and finds that the text search no longer works... It does work if you explicitly set the TextPath property.

Resources