I am having a few issues with using Caliburn Micro's Conductor<>.Collection.OneActive with MahApps.Metro HamburgerMenu. From a few samples, but none of them address my scenario.
All of my code is available in this Github repository.
I want to show a set of panes inside a HamburgerMenu. Each pane has a title and a display name:
public interface IPane : IHaveDisplayName, IActivate, IDeactivate
{
PackIconModernKind Icon { get; }
}
In my case, IPane is implemented using PaneViewModel:
public class PaneViewModel : Screen, IPane
{
public PaneViewModel(string displayName, PackIconModernKind icon)
{
this.Icon = icon;
this.DisplayName = displayName;
}
public PackIconModernKind Icon { get; }
}
This has the following view:
<UserControl x:Class="CaliburnMetroHamburgerMenu.Views.PaneView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
Padding="12"
Background="Pink">
<StackPanel Orientation="Vertical">
<TextBlock Text="Non-bound text" />
<TextBlock x:Name="DisplayName" FontWeight="Bold" />
</StackPanel>
</UserControl>
My shell view model is also quite simple. It inherits from Conductor<IPane>.Collection.OneActive, and takes in a list of panes that it adds to its Items collection:
public class ShellViewModel : Conductor<IPane>.Collection.OneActive
{
public ShellViewModel(IEnumerable<IPane> pages)
{
this.DisplayName = "Shell!";
this.Items.AddRange(pages);
}
}
Now, this is very it gets fuzzy for me. This is an excerpt from ShellView.xaml:
<controls:HamburgerMenu
ItemsSource="{Binding Items, Converter={StaticResource PaneListToHamburgerMenuItemCollection}}"
SelectedItem="{Binding ActiveItem, Mode=TwoWay, Converter={StaticResource HamburgerMenuItemToPane}}">
<ContentControl cal:View.Model="{Binding ActiveItem}" />
<controls:HamburgerMenu.ItemTemplate>
<DataTemplate>
<Grid x:Name="RootGrid"
Height="48"
Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="48" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<iconPacks:PackIconModern
Grid.Column="0"
Kind="{Binding Icon}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="White" />
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
FontSize="16"
Foreground="White"
Text="{Binding Label}" />
</Grid>
</DataTemplate>
</controls:HamburgerMenu.ItemTemplate>
</controls:HamburgerMenu>
To make this work, I rely on two converters (who quite frankly do more than they should have to). One converter takes a ICollection<IPane> and creates a HamburgerMenuItemCollection with HamburgerMenuIconItems that are now contain a two-way link using the Tag properties of both the view model and the menu item.
class PaneListToHamburgerMenuItemCollection : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var viewModels = value as ICollection<IPane>;
var collection = new HamburgerMenuItemCollection();
foreach (var vm in viewModels)
{
var item = new HamburgerMenuIconItem();
item.Label = vm.DisplayName;
item.Icon = vm.Icon;
item.Tag = vm;
vm.Tag = item;
collection.Add(item);
}
return collection;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
The second converter converts between the view model and the menu item using this Tag whenever the SelectedItem changes:
class HamburgerMenuItemToPane : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return ((IPane)value)?.Tag;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return ((HamburgerMenuIconItem)value)?.Tag;
}
}
When I run this code, and click the items in the hamburger menu, the page switches every time. The issue is that when the app first runs, there is no selected pane, and you cannot set one using any of the activation overrides available in ShellViewModel (such as OnViewAttached or OnActivate, or event the constructor), as the converter code that hooks up the Tag hasn't run yet.
My requirements for a working solution:
Caliburn's conductor must be in charge, as there are views and view models further down the stack that depend on the activation logic to run.
It should be possible to activate the first item from Caliburn at some point during the activation of ShellViewModel
Should respect separation of concerns, i.e. the view model should not know that a hamburger menu is being used in the view.
Please see the GitHub repository for a solution that should run straight away.
I believe the issue is caused by the HamburgerMenu_Loaded method inside the control. If there is a selected item before the control loads, the content of the hamburger menu is replaced:
private void HamburgerMenu_Loaded(object sender, RoutedEventArgs e)
{
var selectedItem = this._buttonsListView?.SelectedItem ?? this._optionsListView?.SelectedItem;
if (selectedItem != null)
{
this.SetCurrentValue(ContentProperty, selectedItem);
}
}
In your case, the ContentControl is removed and your Conductor cannot do its job.
I'm trying to see if this behavior can be changed in MahApps directly, by changing the code to something like this:
if (this.Content != null)
{
var selectedItem = this._buttonsListView?.SelectedItem ?? this._optionsListView?.SelectedItem;
if (selectedItem != null)
{
this.SetCurrentValue(ContentProperty, selectedItem);
}
}
Related
I've reached my mental barier and not able to figure it out. I'm sure I'm missing something simple but I'm stuck. The code below is the minimal code required to see my problem but is far away from my production code.
Setting:
I have a WPF window with a DataGrid control that is bound to a business object that includes a collection of assets. For every asset I need to display a user control (SpecialButton) which visibility is determined based on multiple properties of an asset object. When I click on the button (in my example I have an extra button that changes the properties for simplicity) it changes a property of the underlying asset object which should make the control hidden.
Problem
I bind the user control attached property ControlVisibility to the whole asset object {Binding .}
<local:SpecialButton x:Name="buttonOnEachRow" ControlId="{Binding Id}"
ControlVisibility="{Binding ., Converter={StaticResource MyConverter}}"/>
When I change a property of the Asset object PropertyAI expect the MyConverter should run and change the visibility value but it never happen.
What I've tried
I've tried so many things that I even don't remember. The most promising seems to be MultipleBinding but I was not able to figure out how to write the syntax for the ControlVisibility property. I tried some settings on the DataGrid control, changing the way how the user control is updated but no vail.
As a workaround, in my production code, I created a fake property that performs the logic that is currently in the converter and bind the ControlVisibility to the fake property. That works but I have a completely unrelated property in my asset object that is there just because I can't figure out the binding.
The main WPF Window
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
namespace MultiBindingProblem
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
var sut = new BusinessObject() { Caption = "This is the parent object", Assets = new List<Asset>()};
sut.Assets.Add(new Asset() { Name = "Asset 1", Id = 1 });
sut.Assets.Add(new Asset() { Name = "Asset 2", Id = 2 });
sut.Assets.Add(new Asset() { Name = "Asset 3", Id = 3 });
this.DataContext = sut;
}
public event PropertyChangedEventHandler PropertyChanged;
private void BtnCancel_Click(object sender, RoutedEventArgs e)
{
Close();
}
private void BtnChange_Click(object sender, RoutedEventArgs e)
{
((BusinessObject)this.DataContext).Assets[0].PropertyA = true;
//PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Assets"));
}
}
}
XAML
The btnChange is here for simplicity. In my production code the SpecialButton will trigger the property update in my viewmodel
<Window x:Class="MultiBindingProblem.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MultiBindingProblem"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800" WindowStartupLocation="CenterScreen">
<Window.Resources>
<local:TestConverter x:Key="MyConverter" />
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.5*" />
<ColumnDefinition Width="0.5*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="*" />
<RowDefinition Height="100" />
</Grid.RowDefinitions>
<TextBlock x:Name="lblMainObject" Grid.ColumnSpan="2" Grid.Row="0" FontSize="25"
Text="{Binding Caption}" />
<Button x:Name="btnCancel" Content="Cancel" IsCancel="True" IsDefault="True" Grid.Row="2" Grid.Column="1" Click="BtnCancel_Click" />
<DataGrid x:Name="dgrData" Grid.Row="1" Grid.ColumnSpan="2" AutoGenerateColumns="False" CanUserAddRows="False"
ItemsSource="{Binding Assets, NotifyOnSourceUpdated=True}" >
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
<DataGridTemplateColumn Header="Action button" Width="100">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<!--
Here I bind to the whole 'Asset' object to be able to determine if the button should be
visible based on multiple properties. But changing a propety doesn't raise the converter.
I tried use multiple bindings but I was not able to figure out the syntax
-->
<local:SpecialButton x:Name="buttonOnEachRow"
ControlId="{Binding Id}"
ControlVisibility="{Binding ., Converter={StaticResource MyConverter}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Button x:Name="btnChange" Grid.Row="2" Grid.Column="0" Content="Change visibility of the first button" Click="BtnChange_Click" />
</Grid>
</Window>
The user control (SpecialButton)
using System.Windows;
using System.Windows.Controls;
namespace MultiBindingProblem
{
public partial class SpecialButton : UserControl
{
public SpecialButton()
{
InitializeComponent();
}
public static readonly DependencyProperty ControlIdProperty =
DependencyProperty.Register("ControlId", typeof(int),
typeof(SpecialButton));
public int ControlId
{
get { return (int)GetValue(ControlIdProperty); }
set { SetValue(ControlIdProperty, value); }
}
public static readonly DependencyProperty ControlVisibilityProperty =
DependencyProperty.Register("ControlVisibility", typeof(Visibility),
typeof(SpecialButton), new FrameworkPropertyMetadata(Visibility.Visible));
public Visibility ControlVisibility
{
get { return (Visibility)GetValue(ControlVisibilityProperty); }
set { SetValue(ControlVisibilityProperty, value); }
}
private void btnSpecialButton_Click(object sender, RoutedEventArgs e)
{
System.Windows.MessageBox.Show($"The id of the button: {((Button)sender).Tag.ToString()}");
}
}
}
XAML
<UserControl x:Class="MultiBindingProblem.SpecialButton"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MultiBindingProblem"
mc:Ignorable="d"
d:DesignHeight="45" d:DesignWidth="80"
x:Name="parent">
<Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch" DataContext="{Binding ElementName=parent}">
<Button x:Name="btnSpecialButton" Content="Click Me" Click="btnSpecialButton_Click"
Tag="{Binding ControlId}"
Visibility="{Binding ControlVisibility}" />
</StackPanel>
</Grid>
</UserControl>
TestConverter
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace MultiBindingProblem
{
public class TestConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var asset = value as Asset;
if (asset == null) return Visibility.Hidden;
return !(asset.PropertyA || asset.PropertyB) ? Visibility.Visible : Visibility.Hidden;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
Question
Can I somehow use Multibinding?
Or
How to make the converter run when a single property has changed on the asset object?
You would use a MultiBinding with a multi-value converter like this
<Window.Resources>
<local:MultiBooleanToVisibilityConverter x:Key="MyConverter"/>
</Window.Resources>
<local:SpecialButton ...>
<local:SpecialButton.ControlVisibility>
<MultiBinding Converter="{StaticResource MyConverter}">
<Binding Path="PropertyA"/>
<Binding Path="PropertyB"/>
</MultiBinding>
</local:SpecialButton.ControlVisibility>
</local:SpecialButton>
Your current converter implementation looks like it should return Visible if none of the input properties is true. An equivalent multi-value converter could be this:
public class MultiBooleanToVisibilityConverter : IMultiValueConverter
{
public object Convert(
object[] values, Type targetType, object parameter, CultureInfo culture)
{
bool any = values.Any(v => v is bool && (bool)v);
return any ? Visibility.Hidden : Visibility.Visible;
}
public object[] ConvertBack(
object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
In a UWP Project I have a UI which has an ItemsControl bound to a set of Team objects. There is a separate GameController object that has a CurrentTeam property which changes as the Game progresses. I want to be able to have a visual cue in the ItemTemplate for the Team that is the CurrentTeam. An example would be the Current Team's name gets underlined say. The Team objects do not have a reference to the GameController.
One way is to put a flag on each Team, say IsCurrentTeam and bind to that in the ItemTemplate. I don't particularly like this approach as it means when the CurrentTeam changes I've got to loop around all the Teams except the current one, to update their flags.
In WPF I think there might have been a solution using an ObjectDataProvider as it offers the ability to bind to a method, but since I'm in UWP this option is not available.
Does anyone know of a better approach to do this?
Ok, I've prepared an example that shows how this achievable. To work around limitations in UWP it uses a few techniques such as 'data context anchoring' and attached properties.
Here's my support classes, I assume they're somewhat similar to yours:
public class GameControllerViewModel : INotifyPropertyChanged
{
private Team _currentTeam;
public event PropertyChangedEventHandler PropertyChanged;
public GameControllerViewModel(IEnumerable<Team> teams)
{
Teams = teams;
}
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public Team CurrentTeam
{
get { return _currentTeam; }
set
{
if (value != _currentTeam)
{
_currentTeam = value;
OnPropertyChanged();
}
}
}
public IEnumerable<Team> Teams { get; private set; }
}
public class Team
{
public string Name { get; set; }
}
And the code behind of the page:
public sealed partial class GamesPage : Page
{
public GamesPage()
{
this.InitializeComponent();
this.DataContext = new GameControllerViewModel(
new[]
{
new Team { Name = "Team A" },
new Team { Name = "Team B" },
new Team { Name = "Team C" },
new Team { Name = "Team D" }
}
);
}
}
As you can see, the constructor of the page instantiates a GameControllerViewModel with four teams and sets it as the data context of the page.
The page XAML is as follows:
<Page
x:Class="UniversalScratchApp.GamesPage" x:Name="View"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UniversalScratchApp"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Page.Resources>
<local:BoolToFontWeightConverter x:Key="BoolToFontWeightConverter"/>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Current Team:" Margin="4" VerticalAlignment="Center"/>
<ComboBox Grid.Row="0" Grid.Column="1" ItemsSource="{Binding Teams}" SelectedItem="{Binding CurrentTeam, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Stretch" Margin="4">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<ItemsControl Grid.Row="1" Grid.ColumnSpan="2" ItemsSource="{Binding Teams}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" local:TeamProperties.CurrentTeam="{Binding ElementName=View, Path=DataContext.CurrentTeam}" local:TeamProperties.Team="{Binding}" FontWeight="{Binding Path=(local:TeamProperties.IsCurrentTeam), RelativeSource={RelativeSource Mode=Self}, Mode=OneWay, Converter={StaticResource BoolToFontWeightConverter}}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Page>
In the DataTemplate of the ItemsControl you can see that I bind to a three custom attached properties; TeamProperties.CurrentTeam, TeamProperties.Team and TeamProperties.IsCurrentTeam. The attached properties are defined in the following class:
[Bindable]
public static class TeamProperties
{
private static void TeamPropertiesChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
Team team = GetTeam(sender);
Team currentTeam = GetCurrentTeam(sender);
if (team != null && currentTeam != null)
{
SetIsCurrentTeam(sender, team.Equals(currentTeam));
}
}
public static readonly DependencyProperty CurrentTeamProperty = DependencyProperty.RegisterAttached("CurrentTeam", typeof(Team), typeof(TeamProperties), new PropertyMetadata(null, TeamPropertiesChanged));
public static Team GetCurrentTeam(DependencyObject obj)
{
return (Team)obj.GetValue(CurrentTeamProperty);
}
public static void SetCurrentTeam(DependencyObject obj, Team value)
{
obj.SetValue(CurrentTeamProperty, value);
}
public static readonly DependencyProperty TeamProperty = DependencyProperty.RegisterAttached("Team", typeof(Team), typeof(TeamProperties), new PropertyMetadata(null, TeamPropertiesChanged));
public static Team GetTeam(DependencyObject obj)
{
return (Team)obj.GetValue(TeamProperty);
}
public static void SetTeam(DependencyObject obj, Team value)
{
obj.SetValue(TeamProperty, value);
}
public static readonly DependencyProperty IsCurrentTeamProperty = DependencyProperty.RegisterAttached("IsCurrentTeam", typeof(bool), typeof(TeamProperties), new PropertyMetadata(false));
public static bool GetIsCurrentTeam(DependencyObject obj)
{
return (bool)obj.GetValue(IsCurrentTeamProperty);
}
public static void SetIsCurrentTeam(DependencyObject obj, bool value)
{
obj.SetValue(IsCurrentTeamProperty, value);
}
}
To explain, the CurrentTeam and Team properties are set on the dependency object (the textblock) by the bindings. While the Team property can use the current datacontext, the CurrentTeam property must be bound to the 'outer' DataContext. It does this by specifying an x:Name="View" on the Page and using that to 'anchor' the datacontext so it can then be accessed by bindings using the ElementName=View part of the binding.
So, whenever either of these properties change, the IsCurrentTeam property is set on the same dependency object by the TeamPropertiesChanged callback. The IsCurrentTeam property then is bound to the FontWeight property (as it was easier than underlining) with the BoolToFontWeightConverter shown here:
public class BoolToFontWeightConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is bool)
{
return ((bool)value) ? FontWeights.ExtraBold : FontWeights.Normal;
}
else
{
return DependencyProperty.UnsetValue;
}
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotSupportedException();
}
}
Now, when a team is selected in the top combobox (a proxy for whatever mechanism you use to change teams) the appropriate team in the ItemsControl will be displayed in bold.
Works nicely for me. Hope it helps.
In WPF my preferred option would be a DataTrigger, setting the underline property when the DataContext {Binding} of an item equals the contents of CurrentTeam on the parent (use an elementname to refer to that).
As UWP doesn't support Trigger, you can use a DataTriggerBehaviour like in this post.
Initial situation
I have a Windows Phone 8.1 Silverlight app where I have a model that contains several properties as shown below (just an excerpt of 3 properties, it has a lot more).
public class WorkUnit : INotifyPropertyChanged
{
public DateTime? To
{
get { return Get<DateTime?>(); }
set
{
Set(value);
OnPropertyChanged("To");
OnPropertyChanged("ToAsShortTimeString");
}
}
public string ToAsShortTimeString
{
get
{
if (To.HasValue)
{
if (Type == WorkUnitType.StartEnd)
return To.Value.ToString(CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern);
var duration = To.Value - From;
return DateHelper.FormatTime(duration, false);
}
return null;
}
}
public short? Type
{
get { return Get<short?>(); }
set
{
Set(value);
OnPropertyChanged("Type");
}
}
}
I'm using MVVMLight. There are several work units in an ObservableCollection that is bound to a list box on a Windows Phone page. The collection itself is part of a (WorkDay) view model which in turn is bound to the page itself.
What I want to do
I have a lot of properties in my model that are just used to format some properties for the UI. One such is ToAsShortTimeString which returns the time given by the To property, depending on the Type and the From properties, formatted as string.
In order to clean up my model I want to remove such formatter-properties as much as possible and use converters (IValueConverter) as much as possible. One further reason to move away from such properties is that the database that I use (iBoxDB) doesn't have member attributes like [Ignore] that is available for SQLite. So all properties with supported types are stored in the database. However, such formatter properties shouldn't be stored if possible.
What I did - 1st try
I now transformed all properties to converters and most of the time this was no problem. However, ToAsShortTimeString not just uses one property but 3 to format the input. Therefore, in XAML I need to provide either those 3 properties to the value converter or the work unit itself which is bound to the page.
public class WorkUnitToEndTimeStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var workUnit = (WorkUnit) value;
if (workUnit.To.HasValue)
{
if (workUnit.Type == WorkUnitType.StartEnd)
return workUnit.To.Value.ToString(CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern);
var duration = workUnit.To.Value - workUnit.From;
return DateHelper.FormatTime(duration, false);
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
So I changed the binding of the Text property in the TextBlock that shows the formatted To property to the WorkUnit that is bound to the page.
<TextBlock
Grid.Column="2" Grid.Row="0"
Grid.ColumnSpan="2"
Text="{Binding WorkUnit,Converter={StaticResource WorkUnitToEndTimeStringConverter}}"
FontSize="28"
FontFamily="Segoe WP Light"
Foreground="{StaticResource TsColorWhite}"/>
Unfortunately, when the To property changes in the model, even though OnPropertyChanged is called (see model code above), the text block doesn't get updated. I assume the reason is that only those controls are updated where some property is directly bound to the changed model property.
What I did - 2nd try
So as I need 3 properties from WorkUnit in order to correctly format To I changed the binding as follows. I bound Text to WorkUnit.To and set the ConverterParameter to the WorkUnit itself. With this change I hoped that whenever To is changed in the model and the value converter is called, I can format the time because I have all the info provided from the converter parameter (WorkUnit). (I'm not printing the updated converter here but I changed it to accomodate the change on the value and parameter input parameters)
<TextBlock
Grid.Column="2" Grid.Row="0"
Grid.ColumnSpan="2"
Text="{Binding WorkUnit.To,Converter={StaticResource WorkUnitToEndTimeStringConverter},ConverterParameter={Binding WorkUnit}}"
FontSize="28"
FontFamily="Segoe WP Light"
Foreground="{StaticResource TsColorWhite}"/>
Unfortunately, in this case a XamlParseException exception is thrown.
{System.Windows.Markup.XamlParseException: Failed to assign to property 'System.Windows.Data.Binding.ConverterParameter'. [Line: 61 Position: 18] ---> System.InvalidOperationException: Operation is not valid due to the current state of the object.
at MS.Internal.XamlManagedRuntimeRPInvokes.TryApplyMarkupExtensionValue(Object target, XamlPropertyToken propertyToken, Object value)
at MS.Internal.XamlManagedRuntimeRPInvokes.SetValue(XamlTypeToken inType, XamlQualifiedObject& inObj, XamlPropertyToken inProperty, XamlQualifiedObject& inValue)
--- End of inner exception stack trace ---
at System.Windows.Application.LoadComponent(Object component, Uri resourceLocator)}
Question
So is there a way to remove the formatter-property from my model so that I can keep my model as clean as possible? Is there sth. wrong with my converter? Is there any other way that I currently don't know of?
You could have a property in your WorkUnit called EndTimeString
public string EndTimeString
{
get
{
string endTime = null;
if (this.To.HasValue)
{
if (this.Type == WorkUnitType.StartEnd)
{
endTime = this.To.Value.ToString(CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern);
}
else
{
var duration = this.To.Value - this.From;
endTime = DateHelper.FormatTime(duration, false);
}
}
return endTime
}
}
Of course, if To value changes, you want to notify the UI that the EndTimeString has also changed so that it picks up the new value:
public DateTime? To
{
get { return Get<DateTime?>(); }
set
{
Set(value);
OnPropertyChanged("To");
OnPropertyChanged("EndTimeString");
}
}
And then just bind straight to that string:
...Text="{Binding WorkUnit.EndTimeString}" />
Unfortunately you can't bind to parameters.
This would be easy with MultiBinding but that's not available for Windows Phone (as you stated in your comment).
You could implement it yourself but if you are not really into it :), there are implementations trying to mimic this behaviour. One of them can be found from the joy of code.
There is a NuGet package called Krempel's WP7 Library which has above implemented for WP7 but it works on WP8.x as well.
Downside is that it can only bind to elements in visual tree, so you have to use (for lack of a better word) relaying UI elements to get the job done. I have used similar technique myself when I can't bind directly to a property I need to. One case is AppBar, you can't bind directly to enabled property, so instead I use something like
<CheckBox Grid.Row="0" IsEnabled="{Binding AppBarEnabled}"
IsEnabledChanged="ToggleAppBar" Visibility="Collapsed" />
Anyway, below is full example, without any groovy patterns, on how you can achieve multibinding using above library. Try it out and see if it's worth the trouble. Options are that you have "extra" properties in your model or some extra elements and complexity in your view.
I used your WorkUnit model and Converter to make it more useful and easier to understand.
Outcome should be something like this.
MainWindow.xaml
<phone:PhoneApplicationPage
x:Class="WP8MultiBinding.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:Krempel.WP7.Core.Controls;assembly=Krempel.WP7.Core"
xmlns:conv="clr-namespace:WP8MultiBinding"
mc:Ignorable="d"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
Foreground="{StaticResource PhoneForegroundBrush}"
SupportedOrientations="Portrait" Orientation="Portrait"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
shell:SystemTray.IsVisible="True">
<phone:PhoneApplicationPage.Resources>
<conv:WorkUnitToEndTimeStringConverter
x:Key="WorkUnitToEndTimeStringConverter" />
</phone:PhoneApplicationPage.Resources>
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition Height="80" />
<RowDefinition Height="80" />
<RowDefinition />
</Grid.RowDefinitions>
<!-- Multibinding & converter -->
<controls:MultiBinding
x:Name="MultiBinding"
Converter="{StaticResource WorkUnitToEndTimeStringConverter}"
NumberOfInputs="3"
Input1="{Binding ElementName=Type, Path=Text, Mode=TwoWay}"
Input2="{Binding ElementName=From, Path=Text, Mode=TwoWay}"
Input3="{Binding ElementName=To, Path=Text, Mode=TwoWay}" />
<!-- Output from multibinded conversion -->
<TextBox Text="{Binding ElementName=MultiBinding, Path=Output}" Grid.Row="0" />
<!-- Update WorkUnit properties -->
<Button Click="UpdateButtonClick" Grid.Row="1">Test MultiBinding</Button>
<!-- Helper elements, might want to set visibility to collapsed -->
<StackPanel HorizontalAlignment="Center" Grid.Row="2">
<TextBlock x:Name="Type" Text="{Binding WorkUnit.Type, Mode=TwoWay}" />
<TextBlock x:Name="From" Text="{Binding WorkUnit.From, Mode=TwoWay}" />
<TextBlock x:Name="To" Text="{Binding WorkUnit.To, Mode=TwoWay}" />
</StackPanel>
</Grid>
</phone:PhoneApplicationPage>
MainWindow.xaml.cs
using System;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Windows;
using Microsoft.Phone.Controls;
namespace WP8MultiBinding
{
public partial class MainPage : PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
WorkUnit = new WorkUnit()
{
To = DateTime.Now.AddHours(5),
From = DateTime.Now,
Type = WorkUnitType.StartEnd
};
}
public WorkUnit WorkUnit { get; set; }
// Ensure bindings do update
private void UpdateButtonClick(object sender, RoutedEventArgs e)
{
WorkUnit.Type = WorkUnit.Type == WorkUnitType.StartEnd ?
WorkUnit.Type = WorkUnitType.Other :
WorkUnit.Type = WorkUnitType.StartEnd;
WorkUnit.From = WorkUnit.From.AddMinutes(60);
if (WorkUnit.To.HasValue)
WorkUnit.To = WorkUnit.To.Value.AddMinutes(30);
}
}
public enum WorkUnitType
{
StartEnd,
Other
}
public class WorkUnit : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private WorkUnitType _type;
private DateTime _from;
private DateTime? _to;
public WorkUnitType Type
{
get { return _type; }
set { _type = value; OnPropertyChanged(); }
}
public DateTime From
{
get { return _from; }
set { _from = value; OnPropertyChanged(); }
}
public DateTime? To
{
get { return _to; }
set { _to = value; OnPropertyChanged(); }
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
// Multivalue Converter
public class WorkUnitToEndTimeStringConverter : Krempel.WP7.Core.Controls.IMultiValueConverter
{
private const string DateFormat = "M/d/yyyy h:mm:ss tt";
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
// Index: 0 = Type, 1 = From, 2 = To
if (values[2] != null)
{
var type = (WorkUnitType) Enum.Parse(typeof (WorkUnitType), values[0].ToString());
var from = DateTime.ParseExact(values[1].ToString(), DateFormat, CultureInfo.InvariantCulture);
var to = DateTime.ParseExact(values[2].ToString(), DateFormat, CultureInfo.InvariantCulture);
if (type == WorkUnitType.StartEnd)
return to.ToString(CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern);
var duration = to - from;
return duration; // DateHelper.FormatTime(duration, false);
}
return null;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
How can I bind an inverted value of a bool in my xaml?
I know, I know, there are a lot of q&a about this, but there aren't any about a specific case: when the converter is in a view model and the radio button in a user control.
how can I correctly refer to the main window view model data context? In other case I used the code I've posted, but in this case I don't get how to use it.
UPDATE CODE:
namespace ***.ViewModel
{
[ValueConversion(typeof(bool), typeof(bool))]
public class InverseBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (targetType != typeof(bool))
throw new InvalidOperationException("The target must be a boolean");
return !(bool)value;
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
class MainWindowViewModel : MyClass.UtilityClasses.ViewModelBase
{
public MainWindowViewModel(){
SaveCommand = new DelegateCommand(SaveData, CanSave );
UndoCommand = new DelegateCommand(UndoActions, CanSave);
[...]
}
....
}
ANOTHER CODE UPDATE:
In my xaml, I have a devexpress GridControl where I list my observableCollection from db. When I click one element, a layout group is populated with the relative data.
In this layout group, I have:
<StackPanel Margin="0" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Left">
<RadioButton Content="{DynamicResource Locale}" Margin="10,0,0,0" x:Name="rd_LOCALE" VerticalAlignment="Center" IsChecked="{Binding Path=REMOTO, Converter={StaticResource InverseBooleanConverter}}" GroupName="Location" Panel.ZIndex="9" TabIndex="10" />
<RadioButton Content="{DynamicResource Remoto}" Margin="10,0,6,0" x:Name="rd_REMOTO" VerticalAlignment="Center" IsChecked="{Binding REMOTO}" GroupName="Location" Panel.ZIndex="10" TabIndex="11" Tag="PRISMA" />
</StackPanel>
WhenI change from one record to another, the converter gives me error... why? It fails the "if (targetType != typeof(bool))" row. So I tried to change that like:
if (value.getType() != typeof(bool))
but in this way, without fails nothing in the code, it put the wrong value in the radiobutton!
You need to make sure that the value converter is referenced in the XAML
<UserControl.Resources>
<myproject:InverseBooleanConverter x:Key="InverseBooleanConverter" />
</UserControl.Resources>
where my project is the namespace in which the value converter class is defined
The default DataTemplate in a wpf application displays the result of the .ToString() method. I'm developing an application where the default DataTemplate should display nothing.
I've tried:
<Grid.Resources>
<DataTemplate DataType="{x:Type System:Object}">
<Grid></Grid>
</DataTemplate>
</Grid.Resources>
But this doesn't work. Does anyone knows if this is possible without specifiing a specific DataTemplate for every class type in the application?
If you are using the MVVM pattern and have an abstract class which all your ViewModel classes derive from, you can use that class instead of System.Object:
<Grid.Resources>
<DataTemplate DataType="{x:Type vm:VMBase}">
</DataTemplate>
</Grid.Resources>
I know of no way to do this. As per Joe's comment below, WPF specifically disallows specifying a DataTemplate for type Object.
Depending on your exact requirements, it may be easier to search for a DataTemplate that matches the specific type. If you find one, use it. Otherwise, display nothing. For example:
<ContentControl Content="{Binding YourContent}" ContentTemplateSelector="{StaticResource MyContentTemplateSelector}"/>
And in your selector (pseudo-code, obviously):
var dataTemplateKey = new DataTemplateKey() { DataType = theType; };
var dataTemplate = yourControl.FindResource(dataTemplateKey);
if (dataTemplate != null)
{
return dataTemplate;
}
return NulloDataTemplate;
I used Nullable, worked for my situation.
<DataTemplate DataType="{x:Type sys:Nullable}">
<!-- Content -->
</DataTemplate>
I'm not sure about replacing the default DataTemplate, but you can use a ValueConverter to pass display ToString in the case of certain types and an empty string otherwise. Here's some code (note that the typeb textblock doesnt have the converter on it to show what it looks like normally):
.xaml:
<Window x:Class="EmptyTemplate.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:EmptyTemplate"
Title="Window1" Height="300" Width="300">
<Window.Resources>
<loc:AType x:Key="atype"/>
<loc:BType x:Key="btype"/>
<loc:TypeConverter x:Key="TypeConverter"/>
</Window.Resources>
<StackPanel>
<Button Content="{Binding Source={StaticResource atype}, Converter={StaticResource TypeConverter}}"/>
<Button Content="{Binding Source={StaticResource btype}, Converter={StaticResource TypeConverter}}"/>
<TextBlock Text="{Binding Source={StaticResource atype}, Converter={StaticResource TypeConverter}}"/>
<TextBlock Text="{Binding Source={StaticResource btype}}"/>
</StackPanel>
</Window>
.xaml.cs:
namespace EmptyTemplate
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
}
public class AType { }
public class BType { }
public class TypeConverter : IValueConverter
{
public DataTemplate DefaultTemplate { get; set; }
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value.GetType() == typeof(AType))
{
return value.ToString();
}
return DefaultTemplate;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
}
Here a working example about how to do this using a selector (the best way IMO):
public class EmptyDefaultDataTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (item != null)
{
var dataTemplateKey = new DataTemplateKey(item.GetType());
var dataTemplate = ((FrameworkElement) container).TryFindResource(dataTemplateKey);
if (dataTemplate != null)
return (DataTemplate) dataTemplate;
}
return new DataTemplate(); //null does not work
}
}
I discovered something accidentally. I was using a custom dependency property to set the Datacontext on a usercontrol that had a contentcontrol with Datatemplates based on types(entities in my case). Since I had several different kinds of entities my custom dependency property was
` typeof(object)
This was the device I used to bind to the datacontext of the ContentControl.
public object MySelectedItem
{
get { return (object)GetValue(Property1Property); }
set { SetValue(Property1Property, value); }
}
public static readonly DependencyProperty Property1Property
= DependencyProperty.Register(
"MySelectedItem",
typeof(object),
typeof(PromotionsMenu),
new PropertyMetadata(false)
);
Used like this:
MySelectedItem = SomeEntity;
I discovered I could also use it like this:
MySelectedItem = "some text";
And the contextcontrol would print some text as its context.
MySelectedItem = "";
works for a totally blank context.
`