WPF MVVM is there an alternative to IValueConverter - c#

I have several groups of Observable collections of items that are updated on a background thread at a regular interval (30 to 60 seconds). These collections are displayed through ItemsControls on the View. The parent and the items within the control have several display attributes tied to the status of each item.
Status will determine what shape will be displayed next to the text as well as the stroke and fill color of that shape.
Status will determine the background color and text color of the text for that item.
Status will determine if a countdown timer is displayed in the item (Timer has no tie back to the viewmodel)
Status may determine the border color of the parent container.
I am currently performing this logic in individual IValueConverters for each property. It works but it feels cumbersome and spread out. I almost want to somehow subscribe to the PropertyChanged Event in the UI and have it call a single method to render all the display for that item so that all the logic is contained in a single place. Is there a better way to do this or should I just stick to IValueConverters?
Here is an example of what I have.
The collection:
public ObservableCollection<PanelItem> PanelItems1
{
get { return panelItems1; }
set
{
panelItems1 = value;
base.OnPropertyChanged("PanelItems1");
}
}
PanelItem is a small collection of properties that include: Name, Value (Status), Description. The ItemsControls is similar to the following:
<Border Grid.Row="0" Grid.Column="0"
BorderBrush="{Binding Path=PanelGroup1.HighestStatus, Converter={StaticResource ParentBorderColorConverter}}"
BorderThickness="3" Height="Auto" Width="Auto" Margin="0,0,2,0">
<StackPanel Grid.Column="0" Grid.Row="0">
<Label Grid.Row="0" Grid.Column="0" Content="First Group" Style="{StaticResource panelTitle}" />
<ItemsControl ItemsSource="{Binding Path=PanelItems1, Mode=TwoWay}"
ItemTemplate="{StaticResource PanelItemTemplate}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel IsItemsHost="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<!-- Here is the Data Template -->
<DataTemplate x:Key="PanelItemTemplate">
<Viewbox MaxHeight="20" HorizontalAlignment="Left" Cursor="Hand">
<WrapPanel>
<Path Margin="2,2,2,2" StrokeThickness="2">
<Path.Data>
<Binding Path="Status" Mode="OneWay" Converter="{StaticResource ShapeConverter}" />
</Path.Data>
<Path.Fill>
<Binding Path="Status" Mode="OneWay" Converter="{StaticResource ShapeColorConverter}" />
</Path.Fill>
<Path.Stroke>
<Binding Path="Status" Mode="OneWay" Converter="{StaticResource ShapeBorderConverter}" />
</Path.Stroke>
</Path>
<ContentPresenter Content="{Binding Path=Name}" Margin="5,0,0,0" />
</WrapPanel>
</Viewbox>
</DataTemplate>
</StackPanel>
</Border>

Why don't you just declare a normal class that implements the INotifyPropertyChanged interface for your Status object? Just add the required properties into it, such as Geometry, Fill and Stroke, etc... if you did that, you wouldn't need any Converter classes and you could do something like this:
<DataTemplate x:Key="PanelItemTemplate">
<Viewbox MaxHeight="20" HorizontalAlignment="Left" Cursor="Hand" >
<WrapPanel>
<Path Margin="2,2,2,2" StrokeThickness="2" >
<Path.Data>
<Binding Path="Status.Geometry" Mode="OneWay" />
</Path.Data>
<Path.Fill>
<Binding Path="Status.Fill" Mode="OneWay" />
</Path.Fill>
<Path.Stroke>
<Binding Path="Status.Stroke" Mode="OneWay" />
</Path.Stroke>
</Path>
<ContentPresenter Content="{Binding Path=Name}" Margin="5,0,0,0" />
</WrapPanel>
</Viewbox>
</DataTemplate>

It's possible to create just one converter which will include all the conversion logic and return a new class which you will bind to:
public class MyNewConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// Perform all required conversions here and return them as a new type of object
return new
{
Data = ...,
Fill = ...,
Stroke = ...
};
}
}
Then all you need to do is modify your Path's DataContext property to use this converter.
<Viewbox MaxHeight="20" HorizontalAlignment="Left" Cursor="Hand">
<WrapPanel>
<Path Margin="2,2,2,2" StrokeThickness="2" DataContext="{Binding Path=Status, Converter={StaticResource MyNewConverter}}">
<Path.Data>
<Binding Path="Data" Mode="OneWay" />
</Path.Data>
<Path.Fill>
<Binding Path="Fill" Mode="OneWay" />
</Path.Fill>
<Path.Stroke>
<Binding Path="Stroke" Mode="OneWay" />
</Path.Stroke>
</Path>
<ContentPresenter Content="{Binding Path=Name}" Margin="5,0,0,0" />
</WrapPanel>
</Viewbox>

Related

How to avoid repeating blocks of XAML in a menu

I have to create a menu. It has 10 entries and they differ by one parameter.
Entry 1:
<MenuItem Visibility="{Binding MenuSelected.Type, Converter={StaticResource TypeToVisibilityConverter}, ConverterParameter='PAZ', Mode=OneWay}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<i:InvokeCommandAction Command="{Binding CmdContextMenu}" CommandParameter="PAZ" />
</i:EventTrigger>
</i:Interaction.Triggers>
<MenuItem.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="Segoe MDL2 Assets"
Foreground="{Binding MenuSelected.Type, Converter={StaticResource TypeToColorConverter}, ConverterParameter='PAZ', Mode=OneWay}"
Text="{Binding MenuSelected.Type, Converter={StaticResource TypeToIconConverter}, ConverterParameter='PAZ', Mode=OneWay}" />
<TextBlock
Grid.Column="1"
Margin="{StaticResource XSmallLeftMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="PAZ" />
</Grid>
</MenuItem.Header>
</MenuItem>
Entry 2:
<MenuItem Visibility="{Binding MenuSelected.Type, Converter={StaticResource TypeToVisibilityConverter}, ConverterParameter='APP', Mode=OneWay}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<i:InvokeCommandAction Command="{Binding CmdContextMenu}" CommandParameter="APP" />
</i:EventTrigger>
</i:Interaction.Triggers>
<MenuItem.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="Segoe MDL2 Assets"
Foreground="{Binding MenuSelected.Type, Converter={StaticResource TypeToColorConverter}, ConverterParameter='APP', Mode=OneWay}"
Text="{Binding MenuSelected.Type, Converter={StaticResource TypeToIconConverter}, ConverterParameter='APP', Mode=OneWay}" />
<TextBlock
Grid.Column="1"
Margin="{StaticResource XSmallLeftMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="APP" />
</Grid>
</MenuItem.Header>
</MenuItem>
As you can see, the only difference is between PAZ and APP in different points... and same goes on for all the others.
Is there a way I can avoid to repeat it 10 times just changing the 3 letters?
I do not want to use code-behind to create the menu dynamically... but to process it from XAML.
From your question I assume that CmdContextMenu and MenuSelected are properties on your main view model and not in a separate menu item type. If this is different, you have to adapt the code accordingly.
In order to remove the redundant code, create a collection for your menu items in your view model.
public class MyMainViewModel : INotifyPropertyChanged
{
public IEnumerable<string> MyTypes { get; } = new List<string>
{
"PAZ",
"APP"
};
// ...other properties and methods (like "CmdContextMenu" and "MenuSelected" I Assume).
}
Next, you have to change the value converters, because the ConverterParameter is not a dependency property and cannot be bound. Instead, you can use IMultiValueConverters that can bind multiple values. Adapt all of your converters to implement IMultiValueConverter. Here is an example of how a converter could look like. Of course, it depends on your implementation. In essence, use the values array to access bound values.
public class TypeToVisibilityConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values[0].Equals(values[1]) ? Visibility.Visible : Visibility.Collapsed;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Next, create a data template for the header of your menu items. As you can see, the bindings are replaced with MultiBindings that use an IMultiValueConverter as Converter.
The first binding in each block is a RelativeSource binding to access the data context of the parent Menu, because I assume that the MenuSelected property is defined on your main view model. The other empty bindings will bind to the data context of the current menu item, which is an item from the MyTypes collection.
<DataTemplate x:Key="MyMenuItemHeaderTemplate" DataType="{x:Type system:String}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="Segoe MDL2 Assets">
<TextBlock.Foreground>
<MultiBinding Converter="{StaticResource TypeToColorConverter}">
<Binding Path="DataContext.MenuSelected.Type" RelativeSource="{RelativeSource AncestorType={x:Type Menu}}" Mode="OneWay"/>
<Binding/>
</MultiBinding>
</TextBlock.Foreground>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource TypeToIconConverter}">
<Binding Path="DataContext.MenuSelected.Type" RelativeSource="{RelativeSource AncestorType={x:Type Menu}}" Mode="OneWay"/>
<Binding/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<TextBlock Grid.Column="1"
Margin="{StaticResource XSmallLeftMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding}"/>
</Grid>
</DataTemplate>
Create a new header item style that uses this data template. The Visibility also uses a multi-value converter as above. Instead of using an event trigger, you can simply assign the command to the menu item directly and pass a CommandParameter, which is bound to the data context of the current menu item.
<Style x:Key="MyMenuItemStyle" TargetType="{x:Type MenuItem}" BasedOn="{StaticResource {x:Type MenuItem}}">
<Setter Property="HeaderTemplate" Value="{StaticResource MyMenuItemHeaderTemplate}"/>
<Setter Property="Visibility">
<Setter.Value>
<MultiBinding Converter="{StaticResource TypeToVisibilityConverter}">
<Binding Path="DataContext.MenuSelected.Type" RelativeSource="{RelativeSource AncestorType={x:Type Menu}}" Mode="OneWay"/>
<Binding/>
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="Command" Value="{Binding DataContext.CmdContextMenu, RelativeSource={RelativeSource AncestorType={x:Type Menu}}}"/>
<Setter Property="CommandParameter" Value="{Binding}"/>
</Style>
Finally, create a Menu and bind the MyTypes collection, as well as the style.
<Menu ItemsSource="{Binding MyTypes}" ItemContainerStyle="{StaticResource MyMenuItemStyle}"/>
ConverParameter property is not a DependencyProperty - so it cannot be Bound to.
You can use a MultiValue converter instead.
Instead of creating 10 menu items in xaml manually, you should be able to bind an ItemsCollection and define a DataTemplate for MenuItem

Making a ToolTip not visible

There is an icon that I want to always be visible, but I want the tooltip to be conditionally visible. Here is the code that I currently have:
<TextBlock Grid.Row="2"
Grid.Column="0"
VerticalAlignment="Center"
FontSize="15"
Visibility="{Binding IsConnected, Converter={StaticResource BooleanToVisibilityConverter}}">
<fa:ImageAwesome Icon="{Binding Path=BatteryLevelIcon, UpdateSourceTrigger=PropertyChanged}"
Height="20"
Width="20"
Foreground="Green"
Visibility="{Binding IsConnected, Converter={StaticResource BooleanToVisibilityConverter}}" />
<ToolTipService.ToolTip>
<TextBlock Visibility="{Binding IsCharging, Converter={StaticResource InvertedBooleanToVisibilityConverter}}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}%">
<Binding Path="BatteryPercentage" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</ToolTipService.ToolTip>
</TextBlock>
So, I want the tooltip to only show up when IsCharging is false. The issue that I am having is that because the Visibility property is on the tooltip textblock instead of the tooltip itself, setting it to not visible only gives me a empty tooltip, instead of the tooltip not appearing at all. I have tried defining the content of the tooltip (textblock) in UserControls.Resources and then setting the textblock and IsEnabled, but it gave me the error:
a value of type tooltipservice cannot be added to a collection or dictionary of type inlinecolection
It doesn't seem there is an easy way to set the visibility for the tooltip. If anyone has any suggestions it would be greatly appreciated!
You could use ToolTipService.IsEnabled property for the purpose
ToolTipService.IsEnabled="{Binding IsToolTipVisible}"
Where IsToolTipVisible Where is the View Model property which dictates where to enable the tooltip
Complete Code
<TextBlock Grid.Row="2" ToolTipService.IsEnabled="{Binding IsToolTipVisible}"
Grid.Column="0"
VerticalAlignment="Center"
FontSize="15"
Visibility="{Binding IsConnected, Converter={StaticResource BooleanToVisibilityConverter}}">
<fa:ImageAwesome Icon="{Binding Path=BatteryLevelIcon, UpdateSourceTrigger=PropertyChanged}"
Height="20"
Width="20"
Foreground="Green"
Visibility="{Binding IsConnected, Converter={StaticResource BooleanToVisibilityConverter}}" />
<ToolTipService.ToolTip>
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}%">
<Binding Path="BatteryPercentage" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</ToolTipService.ToolTip>
</TextBlock>

C# WPF binding to method with ObjectDataProvider

I have a strange behaviour using ObjectDataProvider. I need to bind a TextBlock with ToString method but, when I enter in method my properties are wrong.
This is my simple ObjectDataProvider:
<Window.Resources>
<ObjectDataProvider x:Key="ToString" MethodName="ToString" ObjectType="{x:Type entities:Season}" />
</Window.Resources>
And this is my ListView:
<ListView Grid.Row="2" Name="lvSeasons" HorizontalContentAlignment="Stretch">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="30" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding Source={StaticResource ToString}}" VerticalAlignment="Center" />
<Button Grid.Column="1" VerticalAlignment="Center" Background="Transparent" BorderBrush="Transparent" Click="btDeleteSeason_Click">
<TextBlock FontFamily="{StaticResource FontAwesome}" Text="" FontSize="20" Foreground="Red" HorizontalAlignment="Center" />
</Button>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
My method simply concat two properties:
public override string ToString()
{
return StartYear + "/" + EndYear;
}
In debug I can see that start and end year are always 0. If I bind my TextBlock using {Binding StartYear} it's correct and value is 2019.
Where can the problem be?
You do not need an ObjectDataProvider. Just write
<TextBlock Text="{Binding}" ... />
WPF will call the ToString method by default.
You do not even need to override ToString when you use a MultiBinding with an appropriate StringFormat:
<TextBlock ...>
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}/{1}">
<Binding Path="StartYear "/>
<Binding Path="EndYear "/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>

Setting custom ToolTip for Border Control in WPF

I am working on internationalizing a WPF application. I am stuck at a situation where a ToolTip to be set near a border control which is to be concatenated like this:
RoleType : Manager
I am binding RoleType in ViewModel which has Manager, TechLead, Engineer etc. RoleType string is fixed and is translated in Resources file. Now in view, I need to concatenate and show the user who has logged in.
I tried MultiBinding but after login, my screen turns blackout. I cannot exit my application.
Here is the code I am trying:
<StackPanel Grid.Row="0" Orientation="Vertical" VerticalAlignment="Top">
<Border BorderBrush="{Binding BorderBrush}" HorizontalAlignment="Center" VerticalAlignment="Center" Height="auto" Width="auto"
Background="Transparent" >
<Border.ToolTip>
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} : {1}">
<Binding Path="Text" ElementName="{languageTranslation:Translate RoleType}" />
<Binding Path="Text" ElementName="{languageTranslation:Translate {Binding UserName}}" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Border.ToolTip>
<ContentControl Height="65" VerticalContentAlignment="Top" HorizontalAlignment="Center" ContentTemplate="{StaticResource UserProfileIcon}"></ContentControl>
</Border>
What am I doing wrong? Converters are not required as it is only strings.

WPF ToolTip Placement Target returns null

I have this code:
<DataTemplate>
<Border>
<Border.ToolTip>
<ToolTip IsEnabled="True"
Placement="Right">
<ToolTip.VerticalOffset>
<MultiBinding Converter="{StaticResource OffsetConverter}"
ConverterParameter="Vertical">
<MultiBinding.Bindings>
<Binding RelativeSource="{RelativeSource Self}"/>
</MultiBinding.Bindings>
</MultiBinding>
</ToolTip.VerticalOffset>
<TextBlock Margin="0"
Padding="0"
TextAlignment="Left"
TextWrapping="Wrap"
MaxWidth="200"
Text="{Binding Description}" FontStyle="Italic">
</TextBlock>
</ToolTip>
</Border.ToolTip>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch" Margin="0">
<Image Height="16" Width="16" Style="{StaticResource AutoCompletionImageStyle}" Margin="0"/>
<Label Content="{Binding DisplayText}" Margin="0" Padding="0"/>
</StackPanel>
</Border>
</DataTemplate>
And this converter :
public object Convert(object[] values, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
AutoCompletionViewModel.OffsetType offsetType = (AutoCompletionViewModel.OffsetType)Enum.Parse(typeof(AutoCompletionViewModel.OffsetType), parameter.ToString());
ToolTip tooltip = values[0] as ToolTip;
Border border = tooltip.PlacementTarget as Border;
double aCalculatedOffset = 0.0;
return aCalculatedOffset;
}
I need to calculate the offset of the tooltip based on some properties of the Border. The tooltip appears relative to the border and manually changing the offset works as expected. However when I try to access the PlacementTarget property of the ToolTip in the converter it is null...
So, although the ToolTip is a direct child of the Border and should have it as a placement target, in the code it is null. Any idea what I am missing here?
I also tried passing the border with <Binding RelativeSource="{RelativeSource AncestorType={x:Type Border}}"/> to no avail.
I am really confused since, if the border is not the logical or visual parent of the ToolTip how does the ToolTip places itself correctly?!
I managed to find a workaround :
<DataTemplate>
<Border ToolTipOpening="OnAutoCompletionBorderToolTipOpening">
<Border.ToolTip>
<ToolTip>
<ToolTip.Style>
<Style>
<EventSetter Event="ToolTip.Opened" Handler="ToolTipOpenedHandler" />
</Style>
</ToolTip.Style>
</ToolTip>
</Border.ToolTip>
</Border>
</DataTemplate>
Code behind:
private void ToolTipOpenedHandler(object sender, RoutedEventArgs e)
{
ToolTip toolTip = (ToolTip)sender;
UIElement target = toolTip.PlacementTarget;
var rect = toolTip.PlacementRectangle;
}
The target here is not null, rather the Border...
I am pretty sure this is some WPF bug. Hopefully someone will be helped by this post.

Categories