Using MVVM instead of MultiValueConverter in WPF XAML - c#

I've been working with XAML for some time now, but when I started I didn't knew much about MVVM so I avoided it. So all the logic in my projects is written in .xaml.cs instead of ViewModel.cs. Now the project is big and I have to move some parts of it to MVVM per request and I have no clue how to do so or where to actually start.
about the problem:
I have two Entities. lets call them A and Secret.
A can have Secret. If A does have it I need to display Secret.Name and Secret.Description beside the A data.
They are binded together with: A.Secret == Secret.Id.
A data and B data can change independently and is received/changed over API calls, but A.Secret and Secret.Id bind will never change.
Secrets is just an array of SecretModel elements
<ListBox ItemsSource="{Binding A, ElementName=uc}" SelectedItem="{Binding SelectedA, ElementName=uc}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto" MaxHeight="300"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="ID:" TextAlignment="Right" />
<TextBlock Grid.Column="1" Text="{Binding Id}"/>
<TextBlock Grid.Row="1" Text="Type:" TextAlignment="Right"/>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Type}" />
<TextBlock Grid.Row="2" Text="Secret:" TextAlignment="Right" Visibility="{Binding Secret, Converter={StaticResource StringToVisibilityConverter}}"/>
<TextBlock Grid.Row="2" Grid.Column="1" Visibility="{Binding Secret, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource SecretIdToNameConverter}">
<Binding Path="Secret"/>
<Binding Path="Secrets" ElementName="uc"/>
</MultiBinding>
</TextBlock.Text>
<TextBlock.ToolTip>
<MultiBinding Converter="{StaticResource SecretIdToDescriptionConverter}">
<Binding Path="Secret"/>
<Binding Path="Secrets" ElementName="uc" />
</MultiBinding>
</TextBlock.ToolTip>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Converters looks like this.
public class SecretIdToDescriptionConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values[0] != null && values[0] != DependencyProperty.UnsetValue && values[0] is string SecretId && values[1] != null && values[1] != DependencyProperty.UnsetValue && values[1] is SecretModel[] Secrets)
{
var tmp = Secrets.FirstOrDefault(secret => secret?.Id == SecretId);
if (tmp != null)
return tmp.Description;
return null;
}
else
{
return null;
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
If anyone could give me some help/pointers on how I should/could migrate from Converters of this type to MVVM or point me to some resource explaining it to me - that would be greatly appreciated.
The things that mostly boggles me:
How should I share/acces the data betwean .xaml.cs or .xaml and ViewModel.cs at this point?
How are calls made to retrieve/give such data?
Is it possible to create method that would return the corresponding SecretModel, so I could just use it after the call in .xaml?
like
<magic ItemsSource= getSecret(A.Secret) >
<TextBlock Text = {Binding Name}/>
</magic>

Related

How to dock listbox to a textbox overlapping other items

I am trying to make textbox with autocomplete drop-down listbox.
The problem lies in that there is not space for listbox, as there are other items just below the textbox. Although, not seen on screenshot, the space between textbox and button will be filled with table.
Is there a way to dock or align a listbox to the bottom of given textbox, regardless to other items in layout?
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="70"/>
<RowDefinition Height="*"/>
<RowDefinition Height="70"/>
</Grid.RowDefinitions>
<TextBox Grid.Row="0" x:Name="txtb_name"></TextBox>
<Grid Grid.Row="2" VerticalAlignment="Bottom">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0" x:Name="btn_givecancel" Content="Cancel" Height="70" FontSize="18.667" Click="btn_givecancel_Click"/>
<Button Grid.Column="1" x:Name="btn_giveaccept" Content="Accept" Height="70" FontSize="18.667" Click="btn_giveaccept_Click"/>
</Grid>
</Grid>
Here's My Implementation:
Have a Converter that takes Total possible auto suggest item count and current view count:
<local:IntToVisibilityConverter x:Key="IntToVisibilityConverter"/>
Then here's is the Form with TextBox, ListBox and the Button controls.
<Grid Margin="50">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBox Grid.Row="0" Text="{Binding UserText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" DockPanel.Dock="Top"/>
<ListBox Grid.Row="1" Grid.RowSpan="2" VerticalAlignment="Top" MaxHeight="55" ItemsSource="{Binding SuggestionsFiltered, UpdateSourceTrigger=PropertyChanged}"
Canvas.ZIndex="1">
<ListBox.Visibility>
<MultiBinding Converter="{StaticResource IntToVisibilityConverter}">
<Binding Path="MaxCount"/>
<Binding Path="SuggestionsFiltered.Count"/>
</MultiBinding>
</ListBox.Visibility>
</ListBox>
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0 50 0 0">
<Button Height="20" Width="100">Clear</Button>
<Button Height="20" Width="100" Margin="10 0 0 0">Accept</Button>
</StackPanel>
</Grid>
Finally, here's my DataContext:
public class TheDataContext
{
public TheDataContext()
{
FillData();
_SuggestionsFiltered = CollectionViewSource.GetDefaultView(_SuggestionSource);
_SuggestionsFiltered.Filter = obj =>
{
var opt = obj as string;
if (string.IsNullOrEmpty(_UserText) || _UserText.Length == 0)
return true;
return string.Join("", opt.Take(_UserText.Length)) == _UserText;
};
}
private void FillData()
{
_SuggestionSource = new List<string>();
_SuggestionSource.Add("Alpha");
_SuggestionSource.Add("Alpines");
_SuggestionSource.Add("Bravo");
_SuggestionSource.Add("Brood");
_SuggestionSource.Add("Charlie");
_SuggestionSource.Add("Charles");
_SuggestionSource.Add("Charlotte");
}
private string _UserText;
public string UserText
{
get => _UserText;
set
{
_UserText = value;
_SuggestionsFiltered.Refresh();
}
}
private List<string> _SuggestionSource;
public int MaxCount => _SuggestionSource.Count;
private ICollectionView _SuggestionsFiltered;
public ICollectionView SuggestionsFiltered
{
get => _SuggestionsFiltered;
}
}
Notice all the code around ICollectionView. Also, I have forcefully set some margins in xaml to show case that Listbox is drawn over other controls (courtsey ZIndex).
If you take this code, remember to handle selected event on ListBox, then set the textBox Text to this value. Also hide the listbox. Bit of a jugglery there.
Lastly, here is the Converter, if you are interested:
public class IntToVisibilityConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
int maxCount = System.Convert.ToInt32(values[0]);
int count = System.Convert.ToInt32(values[1]);
if (count > 0 && count != maxCount)
return Visibility.Visible;
return Visibility.Collapsed;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

Grouping ListBox with Date

I have no idea how to achieve this, but I have a date and time column in a ListBox. The Date column should not display if the date was already in there. I know that in combination with ListCollectionView and Listview/DataGrids, it is propably possible. But can I achieve this with a ListBox and a List. Keep in mind I am using the MVVM principle. This is my listbox:
<ListBox Grid.Row="2" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ItemsSource="{Binding Schedules}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Background="Transparent">
<Grid Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" Grid.Row="0" Text="{Binding MyDateTime, StringFormat='d' }" HorizontalAlignment="Left" VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" Grid.Row="0" Text="{Binding MyDateTime, StringFormat='t'}" HorizontalAlignment="Left" VerticalAlignment="Center"/>
<TextBlock Grid.Column="2" Grid.Row="0" Text="{Binding SomeText}" TextTrimming="WordEllipsis" LineStackingStrategy="MaxHeight" MaxHeight="20" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</Grid>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I have made this in excel to give an example of what I am trying to achieve:
I want the affect where I have highlighted with yellow
Converters
// Order Schedules using System.Linq
public class ToOrderedListConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
List<ScheduleItem> schedules = (List<ScheduleItem>)value;
var subset = from item in schedules
orderby item.MyDateTime.TimeOfDay
orderby item.MyDateTime.ToString("yyyy/MM/dd") descending
select item;
return subset.ToList();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
// Show only first occurrence of date
public class DateToVisibilityConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
DateTime currentItem = (DateTime)values[0];
List<ScheduleItem> schedules = (List<ScheduleItem>)values[1];
ScheduleItem firstOccurrence =
schedules.Find(item => item.MyDateTime.Year == currentItem.Year
&& item.MyDateTime.Month == currentItem.Month
&& item.MyDateTime.Day == currentItem.Day);
if (firstOccurrence.MyDateTime == currentItem)
return Visibility.Visible;
else return Visibility.Collapsed;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
XAML
<ListBox Grid.Row="2" ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ItemsSource="{Binding Schedules, Converter={StaticResource ToOrderedListConverter}}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Background="Transparent">
<Grid Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" Grid.Row="0" Text="{Binding MyDateTime, StringFormat='dd/MM/yyyy'}" HorizontalAlignment="Left" VerticalAlignment="Center">
<TextBlock.Visibility>
<MultiBinding Converter="{StaticResource DateToVisibilityConverter}">
<Binding Path="MyDateTime"/>
<Binding RelativeSource="{RelativeSource AncestorType={x:Type ListBox}}"
Path="ItemsSource"/>
</MultiBinding>
</TextBlock.Visibility>
</TextBlock>
<TextBlock Grid.Column="1" Grid.Row="0" Text="{Binding MyDateTime, StringFormat='t'}" HorizontalAlignment="Left" VerticalAlignment="Center"/>
<TextBlock Grid.Column="2" Grid.Row="0" Text="{Binding SomeText}" TextTrimming="WordEllipsis" LineStackingStrategy="MaxHeight" MaxHeight="20" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</Grid>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
You can use this approach for your requirements.
https://code.msdn.microsoft.com/windowsdesktop/CollectionView-Tips-MVVM-d6ebb4a7#content

Pass current item to ValueConverter

I have a ListBox where I display all order positions. I need to display a price. I created a ValueConverter which takes a OrderPosition object and returns my price as double.
Formula: Amount * Product.Price (Amount and Product are properties in OrderPosition)
My XAML just won't display anything:
<ListBox Grid.Row="1" Grid.Column="0" Margin="3" ItemsSource="{Binding SelectedOrder.OrderPositions}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}x {1}">
<Binding Path="Amount" />
<Binding Path="Product.Label" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<TextBlock Text="{Binding /, Converter={StaticResource PositionPriceConverter}, StringFormat={}{0:c}}" Grid.Column="1"
TextAlignment="Right" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Here is my converter:
public class PositionPriceConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var position = (OrderPosition)value;
return position.Amount * position.Product.Price;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
At the moment you set Path=/ which binds it to CollectionView.CurrentItem
When the source is a collection view, the current item can be specified with a slash (/). For example, the clause Path=/ sets the binding to the current item in the view. When the source is a collection, this syntax specifies the current item of the default collection view.
You can achieve what you're after by setting Path=. or not setting Path altogether.
<TextBlock Text="{Binding Path=., Converter=...}
or
<TextBlock Text="{Binding Converter=...}
but be aware that it will not trigger update when either Amount or Product.Price will change so maybe MultiBinding and IMultiValueConverter would be better option.
I Am not sure if that path you provided to the binding is legal ({Binding /, Converter....).
try to change it in:
<TextBlock Text="{Binding Converter={StaticResource PositionPriceConverter}, StringFormat={}{0:c}}" Grid.Column="1" TextAlignment="Right" />
or
<TextBlock Text="{Binding Path=., Converter={StaticResource PositionPriceConverter}, StringFormat={}{0:c}}" Grid.Column="1" TextAlignment="Right" />
<ListBox Grid.Row="1"
Grid.Column="0"
Margin="3"
ItemsSource="{Binding SelectedOrder.OrderPositions}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}x {1}">
<Binding Path="Amount" />
<Binding Path="Product.Label" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<TextBlock Grid.Column="1">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource PositionPriceConverter}" StringFormat="{}{0}x {1}">
<Binding Path="Amount" />
<Binding Path="Product.Label" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Change the converter like this,
public class PositionPriceConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var amt = (double)values[0];
var price = (double) values[1];
return amt * price;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

Display the selected Item in listbox to textbox

I have a listbox which stores records of XML file. My XMLfile has multiple elements like Name, Destination, employee ID. The listbox would display all the name contents of the XMLfile.
<Information>
<Name>Goutham</Name>
<Destination>dar</Destination>
<EmployeeID>da</EmployeeID>
</Information>
<Information>
<Name>Adam</Name>
<Destination>ads</Destination>
<EmployeeID>dwa</EmployeeID>
</Information>
<Information>
<Name>dwad</Name>
<Destination>wadwa</Destination>
<EmployeeID>daw</EmployeeID>
</Information>
The listbox displays all the different names like Goutham,Adam,dwad.
Now if I select Goutham in the listbox, I need to display all the details of Goutham on the textbox. How do i do it?
This is my xaml file
<ListBox Height="251" HorizontalAlignment="Left" Margin="330,23,0,0" Name="listBox1" VerticalAlignment="Top" Width="170" IsSynchronizedWithCurrentItem="True" DisplayMemberPath="Name" ItemsSource="{Binding}"/>
<TextBox Height="23" HorizontalAlignment="Left" Margin="141,42,0,0" Name="textBox1" VerticalAlignment="Top" Width="173" Text="{Binding ElementName= listbox1, Path=SelectedItem.Name}"/>
If the listbox fills up it should allready work, you just have a typo in the ElementName binding it should be listBox1.
Also if you want all the details not just the Name you could make a converter and leave only SelectedItem in the binding path. The converter would then format the details form the Information instance returning a string to be displayed in the texbox, or just use several textboxes.
Another option is to have MultiBinding with StringFormat:
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="{0} {1} {2}">
<Binding ElementName="listbox1", Path="SelectedItem.Name" />
<Binding ElementName="listbox1", Path="SelectedItem.Destination" />
<Binding ElementName="listbox1", Path="SelectedItem.EmployeeID" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
You can bind ListBox to once specific property say Name & then bind TextBlock to this listbox. Depending upon your need, you can have individual TextBlocks for each property or a single TextBlock... as shown below...
<Window x:Class="ListBoxDataBinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:ListBoxDataBinding"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<c:EmployeeConverter x:Key="myEmployeeConverter"/>
</Window.Resources>
<StackPanel Orientation="Vertical">
<ListBox x:Name="lbEmployee">
<ListBox.ItemTemplate>
<DataTemplate>
<Label Content="{Binding Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Width="248" Height="24" Text="Employee Name" />
<TextBlock Grid.Row="0" Grid.Column="1" Width="248" Height="24" Text="{Binding ElementName=lbEmployee, Path=SelectedItem.Name}" />
<TextBlock Grid.Row="1" Grid.Column="0" Width="248" Height="24" Text="Employee EmployeeId" />
<TextBlock Grid.Row="1" Grid.Column="1" Width="248" Height="24" Text="{Binding ElementName=lbEmployee, Path=SelectedItem.EmployeeId}" />
<TextBlock Grid.Row="2" Grid.Column="0" Width="248" Height="24" Text="Employee Destination" />
<TextBlock Grid.Row="2" Grid.Column="2" Width="248" Height="24" Text="{Binding ElementName=lbEmployee, Path=SelectedItem.Destination}" />
<TextBlock Grid.Row="3" Grid.Column="0" Width="248" Height="24" Text="Employee Details" />
<TextBlock Grid.Row="3" Grid.Column="2" Width="248" Height="24">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource myEmployeeConverter}" ConverterParameter="FormatEmployee">
<Binding ElementName="lbEmployee" Path="SelectedItem.Name" />
<Binding ElementName="lbEmployee" Path="SelectedItem.EmployeeId" />
<Binding ElementName="lbEmployee" Path="SelectedItem.Destination" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
</StackPanel>
Code behind....
public partial class MainWindow : Window
{
private List<Employee> _lstEmployees;
public MainWindow()
{
InitializeComponent();
Loaded += new RoutedEventHandler(MainWindow_Loaded);
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
_lstEmployees = new List<Employee>();
_lstEmployees.Add(new Employee { Name = "Goutham", EmployeeId = "da", Destination = "dar" });
_lstEmployees.Add(new Employee { Name = "Adam", EmployeeId = "dwa", Destination = "ads" });
_lstEmployees.Add(new Employee { Name = "dwad", EmployeeId = "daw", Destination = "wadwa" });
lbEmployee.ItemsSource = _lstEmployees;
}
}
public class EmployeeConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
string name;
switch ((string)parameter)
{
case "FormatEmployee":
if (values[0] is String)
{
name = values[0] + ", " + values[1] + ", " + values[2];
}
else
{
name = String.Empty;
}
break;
default:
name = String.Empty;
break;
}
return name;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
string[] splitValues = ((string)value).Split(' ');
return splitValues;
}
}

Concatenate binding text and static text in source image

Is it possible to concatenate binding text with a static text in source image
For example:
<Image Name="ImagePlace" Source="http://site.com/image/architecture.png" Grid.Column="0" />
<Image Name="ImagePlace" Source="{Binding Path=Ico}" Grid.Column="0" />
And i would like to concatenate the two sources.
I have a list of object named Category that contains a field Icon that contains for example "architecture.png". I bind it in a list.
The url of site doesn't change but the image changes always.
<ListBox x:Name="ListboxCategories">
<ListBox.ItemTemplate>
<DataTemplate>
<Button Name="category" Tag="{Binding Path=Id}" Tap="category_Tap_1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Image Name="ImagePlace" Source="http://www.test.com/assets/images/icons/tags/architecture.png" Grid.Column="0" />
<TextBlock Text="{Binding Path=Title}" Grid.Column="1" HorizontalAlignment="Center" />
<Image Name="chevron" Grid.Column="2" Source="/Assets/AppBar/White/appbar.chevron.right.png" HorizontalAlignment="Right" Width="50" Height="50" />
</Grid>
</Button>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The cleanest way (well, depending on your architecture) would be to add a property in your object to do the concatenation:
public string FullUri
{
get
{
return "http://site.com/image/" + this.Ico
}
}
Another way is to use a custom converter to do the concatenation.
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null)
{
return null;
}
return "http://site.com/image/" + value.ToString();
}

Categories