I'm trying to change the ColumnSpan based on a value from my ViewModel in a UWP for Windows 10 using the following:
<Setter Target="ProgramView.ColumnSpan" Value="{Binding
IsProgramViewVisible, Converter={StaticResource OneIfVisibleConverter}}"/>
I'm having 2 problems:
a) It doesn't allow me to bind
b) It can't find the converter even though it's declared in my Page's resources.
When I move my mouse over the above, it displays an error:
Catastrophic Failure: (Exception from HRESULT: 0X8000FFFF (E_UNEXPECTED)
The error occurs whether I define my converter or not, so I'm assuming the problem is with the binding.
Is there a way I can achieve this?
Thanks.
Since we cant provide an accurate solution without seeing your code.
Here is a check list you can follow to find out the bug:
1.Debug your Converter and check if it is returning desired value for all test cases.
2.Check if all the names are proper and there isn't any typo in ur xaml.
Here is an implementation for binding column span with an vm and updating it with a command bound to the click event.
<Page.Resources>
<local:BoolToColumnSpanConverter x:Key="BoolToColumnSpanConverter" />
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.DataContext>
<local:Items />
</Grid.DataContext>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="100" />
<ColumnDefinition Width="100" />
<ColumnDefinition Width="100" />
</Grid.ColumnDefinitions>
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Red"
Grid.ColumnSpan="{Binding span, Converter={StaticResource BoolToColumnSpanConverter}}" />
<Button Click="Button_Click"
Content="change span"
Grid.ColumnSpan="2"
Grid.Column="2"
Margin="5"
Command="{Binding ChangeSpanCommand, Mode=OneWay}" />
</Grid>
the code behind:
The Converter just converts true to 2 and false to 1
The command is bound to a method in the VM called UpdateSpan which just turns the boolean inverse.
When the button is pressed , since it is bound to the command the command is called, since it just returns a new relaycommand with the UpdateSpan as a parameter this method is executed .. which will update the span boolean triggering a change which is notified by the System thru the OnPropertyChanged event and the value converter is executed turning the columnspan to 1 and 2 .
public class BoolToColumnSpanConverter : IValueConverter
{
public object Convert( object value , Type targetType , object parameter , string language )
{
var b = (bool)value;
return b ? 2 : 1;
}
public object ConvertBack( object value , Type targetType , object parameter , string language )
{
throw new NotImplementedException();
}
}
public class Items : INotifyPropertyChanged
{
private bool _span;
public bool span
{
get { return _span; }
set
{
if (value != _span) _span = value;
OnPropertyChanged();
}
}
public ICommand ChangeSpanCommand {
get
{
return new RelayCommand(() => UpdateSpan());
}
}
public Items()
{
span = true;
}
public void UpdateSpan()
{
span = !span;
}
#region Notify Property Changed Members
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged( [CallerMemberName]string propertyName = null )
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this , new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
public class RelayCommand : ICommand
{
public event EventHandler CanExecuteChanged;
public bool CanExecute( object parameter )
{
return true;
}
public void Execute( object parameter )
{
this._action();
}
private Action _action;
public RelayCommand( Action action )
{
this._action = action;
}
}
Here's the answer to my problem.
Binding directly in a Setter is not allowed for Universal App (Windows Store & Windows Phone Apps), but works ok with WPF.
<Setter Target="ProgramView.ColumnSpan" Value="{Binding
IsProgramViewVisible, Converter={StaticResource OneIfVisibleConverter}}"/>
Triggers/DataTriggers are not supported in XAML and have been replaced by the VisualStateManager as explained in DataTriggers in WinRT post in StackOverflow.
I found various explanations on how to resolve this so here are a few links you might also find helpful:
Handling VisualState in Universal apps with Behavior SDK and MVVM
A Behavior to handle VisualState in Universal apps with MVVM
Using a Style Selector
Adventures in Windows 8: Placing items in a GridView with a ColumnSpan or RowSpan (also mentioned by #Bit)
So my solution as mentioned above was to use the VisualStateManager, more specifically, `DataTriggerBehavior.
One mistake which cost me a lot of time was to try to use this in conjunction with existing AdaptiveTrigger MinWindowWidth as I wanted to set my a VisualState based on the size and based on a binded property. This turned out to be a nightmare and it's a shame that there isn't a better mixture of the 2. Maybe there is a solution and I'm still missing something but for now my solution was a follows:
Define my various VisualStates and set the various properties within it:
<VisualState x:Name="ListOnly">
<VisualState.Setters>
<Setter Target="ProgramList.(Grid.Column)" Value="0" />
....
</VisualState.Setters>
</VisualState>
Check my app's orientation and width from the MainPage.Xaml in the Page_SizeChanged event and create a static property in the App.cs and set it from there:
private void Page_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (ApplicationView.GetForCurrentView().Orientation ==
ApplicationViewOrientation.Landscape)
{
App.IsLandscape = (ApplicationView.GetForCurrentView()
.VisibleBounds.Width < 600) ? false : true;
}
else
{
App.IsLandscape = false;
}
}
From the relevant page that loaded in the Frame of MainPage i.e. ListPage.xaml for example, I created a function called SizeChanged in the relevant ViewModel i.e. ListPageViewModel and I call this from within the Page_SizeChange event of the page:
private void Page_SizeChanged(object sender, Windows.UI.Xaml.SizeChangedEventArgs e)
{
this.GetViewModel.SizeChanged();
}
The SizeChange method in the relevant ViewModel will change a property that's binded to the DataTrigger based on my app's orientation and size:
public void SizeChanged()
{
if (!App.IsLandscape)
{
...
this.ActivePart = ActivePartEnum.ListOnly.ToString();
...
}
else
{
...
this.ActivePart = ActivePartEnum.Both.ToString();
...
}
}
Finally in the relevant XAML Page, call the DataTriggerBehaviour:
<Interactivity:Interaction.Behaviors>
<Core:DataTriggerBehavior Binding="{Binding ActivePart}"
ComparisonCondition="Equal" Value="ListOnly">
<Core:GoToStateAction StateName="ListOnly" />
</Core:DataTriggerBehavior>
....
</Interactivity:Interaction.Behaviors>
With the above, you should be able to apply a specific template based on orientation and size of your app and can be expanded if needed. One thing I will be looking into further is behaviour but couldn't get it to work and had to move on for now, but that would be an even better solution I think.
Hope this helps anyway and thanks for everyone's feedback.
Related
I am creating a WPF-based plugin (for Revit, an architectural 3D modelling software, but this shouldn't matter) which is quite complex and I'm getting kind of lost.
The WPF Window is composed by 2 tabs and each Tab is a custom UserControl that I'm inserting in the TabItem through a Frame. The Main Window has a ViewModel where the data is bound.
One of the tabs helps with the creation of floors in a 3D model
part of MainWindow.xaml
<TabItem Name="LevelsTab" Header="Levels" HorizontalContentAlignment="Left">
<ScrollViewer >
<Frame Name="LevelsContent" Source="LevelsTab.xaml"/>
</ScrollViewer>
</TabItem>
The LevelsTab.xaml UserControl is really barebone and just contains buttons to create or remove a custom UserControl I created to represent graphically a floor in the UI (screenshot below). This very simple as well:
LevelDefinition.xaml
<UserControl x:Class="RevitPrototype.Setup.LevelDefinition" ....
<Label Grid.Column="0" Content="Level:"/>
<TextBox Name="LevelName" Text={Binding <!--yet to be bound-->}/>
<TextBox Name="LevelElevation" Text={Binding <!--yet to be bound-->}/>
<TextBox Name="ToFloorAbove" Text={Binding <!--yet to be bound-->}/>
</UserControl>
When the user clicks the buttons to add or remove floors in LevelsTab.xaml, a new LevelDefinition is added or removed to the gird.
Each LevelDefinition will be able to create a Level object from the information contained in the different TextBox elements, using MVVM. Eventually, in the ViewModel, I should have a List<Level> I guess.
Level.cs
class Level
{
public double Elevation { get; set; }
public string Name { get; set; }
public string Number { get; set; }
}
Each LevelDefinition should be sort of bound to the previous one though, as the floor below contains the information of the height to the Level above. The right-most TextBox in LevelDefinition.xaml indicated the distance between the current floor and the floor above, hence the Height `TextBox should just be the sum of its height PLUS the distance to the level above:
Of course the extra level of difficulty here is that if I change distance to the level above in one floor, all the floors above will have to update the height. For example: I change LEVEL 01 (from the pic) to have 4 meters to the level above, LEVEL 02's height will have to update to become 7m (instead of 6) and LEVEL 03's will have to become 10m.
But at this point I'm very lost:
How do I get this logic of getting the floor height bound to the info in the floor below?
How do I implement MVVM correctly in this case?
I hope I managed to explain the situation correctly even though it's quite complex and thanks for the help!
If you intend to make your Level items editable, you have to implement INotifyPropertyChanged. I created a level view model for demonstration purposes and added a property OverallElevation that represents the current elevation including that of previous levels.
public class LevelViewModel : INotifyPropertyChanged
{
private string _name;
private int _number;
private double _elevation;
private double _overallElevation;
public LevelViewModel(string name, int number, double elevation, double overallElevation)
{
Number = number;
Name = name;
Elevation = elevation;
OverallElevation = overallElevation;
}
public string Name
{
get => _name;
set
{
if (_name == value)
return;
_name = value;
OnPropertyChanged();
}
}
public int Number
{
get => _number;
set
{
if (_number == value)
return;
_number = value;
OnPropertyChanged();
}
}
public double Elevation
{
get => _elevation;
set
{
if (_elevation.CompareTo(value) == 0)
return;
_elevation = value;
OnPropertyChanged();
}
}
public double OverallElevation
{
get => _overallElevation;
set
{
if (_overallElevation.CompareTo(value) == 0)
return;
_overallElevation = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
You can bind these properties to your LevelDefinition user control. I adapted your sample, because it is incomplete. Since the overall elevation is calculated, I set the corresponding TextBox to be read-only, but you should really use a TextBlock or a similar read-only control instead.
<UserControl x:Class="RevitPrototype.Setup.LevelDefinition"
...>
<UserControl.Resources>
<Style TargetType="{x:Type TextBox}" BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="Margin" Value="5"/>
</Style>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="Level:"/>
<TextBox Grid.Column="1" Name="LevelName" Text="{Binding Name}"/>
<TextBox Grid.Column="2" Name="LevelElevation" Text="{Binding OverallElevation}" IsReadOnly="True"/>
<TextBox Grid.Column="3" Name="ToFloorAbove" Text="{Binding Elevation}"/>
</Grid>
</UserControl>
Since you did not provide your tab view model, I created one for reference. This view model exposes an ObservableCollection of levels, a GroundFloor property and commands to add and remove levels. I use a DelegateCommand type, but you may use a different one.
On each add of a level, you subscribe to the PropertyChanged event of the new level and on removal you unsubscribe to prevent memory leaks. Now, whenever a property changes on a LevelViewModel instance, the OnLevelPropertyChanged method is called. This method checks, if the Elevation property was changed. If it was, the UpdateOverallElevation method is called, which recalculates all overall elevation properties. Of course you could optimize this to only recalculate the levels above the current one passed as sender.
For a more robust implementation, you should subscribe to the CollectionChanged event of the Levels collection, so can subscribe to and unsubscribe from the PropertyChanged events of level items whenever you add, remove or modify the collection in other ways than through the commands like restoring a persisted collection.
public class LevelsViewModel
{
private const string GroundName = "GROUND FLOOR";
private const string LevelName = "LEVEL";
public ObservableCollection<LevelViewModel> Levels { get; }
public LevelViewModel GroundFloor { get; }
public ICommand Add { get; }
public ICommand Remove { get; }
public LevelsViewModel()
{
Levels = new ObservableCollection<LevelViewModel>();
GroundFloor = new LevelViewModel(GroundName, 0, 0, 0);
Add = new DelegateCommand<string>(ExecuteAdd);
Remove = new DelegateCommand(ExecuteRemove);
GroundFloor.PropertyChanged += OnLevelPropertyChanged;
}
private void ExecuteAdd(string arg)
{
if (!double.TryParse(arg, out var value))
return;
var lastLevel = Levels.Any() ? Levels.Last() : GroundFloor;
var number = lastLevel.Number + 1;
var name = GetDefaultLevelName(number);
var overallHeight = lastLevel.OverallElevation + value;
var level = new LevelViewModel(name, number, value, overallHeight);
level.PropertyChanged += OnLevelPropertyChanged;
Levels.Add(level);
}
private void ExecuteRemove()
{
if (!Levels.Any())
return;
var lastLevel = Levels.Last();
lastLevel.PropertyChanged -= OnLevelPropertyChanged;
Levels.Remove(lastLevel);
}
private void OnLevelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(LevelViewModel.Elevation))
return;
UpdateOverallElevation();
}
private static string GetDefaultLevelName(int number)
{
return $"{LevelName} {number:D2}";
}
private void UpdateOverallElevation()
{
GroundFloor.OverallElevation = GroundFloor.Elevation;
var previousLevel = GroundFloor;
foreach (var level in Levels)
{
level.OverallElevation = previousLevel.OverallElevation + level.Elevation;
previousLevel = level;
}
}
}
The view for the levels tab item could look like below. You can use a ListBox with your LevelDefinition user control as item template to display the levels. Alternatively, you could use a DataGrid with editable columns for each property of the LevelViewModel, which would be more flexible for users.
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListView ItemsSource="{Binding Levels}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<local:LevelDefinition/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListView>
<DockPanel Grid.Row="1" Margin="5">
<Button DockPanel.Dock="Right" Content="-" MinWidth="50" Command="{Binding Remove}"/>
<Button DockPanel.Dock="Right" Content="+" MinWidth="50" Command="{Binding Add}" CommandParameter="{Binding Text, ElementName=NewLevelElevationTextBox}"/>
<TextBox x:Name="NewLevelElevationTextBox" MinWidth="100"/>
</DockPanel>
<local:LevelDefinition Grid.Row="2" DataContext="{Binding GroundFloor}"/>
</Grid>
This is a simplified example, there is no input validation, invalid values are ignored on adding.
I've managed to implement this using a multi-binding converter.
Assuming that you set up the multi-converter as a static resource somewhere, the TextBlock to display the value is:
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource ElevationMultiConverter}">
<MultiBinding.Bindings>
<Binding Path="" />
<Binding Path="DataContext.Levels" RelativeSource="{RelativeSource AncestorType={x:Type ItemsControl}}" />
</MultiBinding.Bindings>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
The converter itself looks like this:
class ElevationMultiConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var item = values[0] as Level;
var list = values[1] as IList<Level>;
var lowerLevels = list.Where(listItem => list.IndexOf(listItem) <= list.IndexOf(item));
var elevation = lowerLevels.Sum(listItem => listItem.Height);
return elevation.ToString();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
In this example, it depends on the specific order of items in the list to determine whether a level is above or below another; you could use a property, or whatever else.
I didn't use a framework for this example so I needed to implement INotifyPropertyChanged everywhere myself. In the MainViewModel, this meant adding a listener to each Level element's PropertyChanged event to trigger the multibinding converter to have 'changed'. In total, my MainViewModel looked like this:
class MainViewModel :INotifyPropertyChanged
{
public ObservableCollection<Level> Levels { get; set; }
public MainViewModel()
{
Levels = new ObservableCollection<Level>();
Levels.CollectionChanged += Levels_CollectionChanged;
}
private void Levels_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
foreach(var i in e.NewItems)
{
(i as Level).PropertyChanged += MainViewModel_PropertyChanged;
}
}
private void MainViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Levels)));
}
public event PropertyChangedEventHandler PropertyChanged;
}
How it works:
A new Level is added to the collection, and it's PropertyChanged event is listened to by the containing view model. When the height of a level changes, the PropertyChanged event is fired and is picked up by the MainViewModel. It in turn fires a PropertyChanged event for the Levels property. The MultiConverter is bound to the Levels property, and all changes for it trigger the converters to re-evaluate and update all of the levels combined height values.
I'm using a GridView in a UserControl to display a five by four square of graphical buttons that allow selection of a Lesson.
This is in a Windows 8.1 Store App that I'm upgrading to Windows 10 UWP.
I previously used Tap and Right-Tap actions to select a Lesson or activate the CommandBar to perform related actions for a Lesson through the SelectionChanged event. However, there have been changes to how Interactions now work under Windows 10, I have been unable to get the Gridview to work at all with binding the SelectedItem to the selected LessonButton in the view model, nor the SelectionChanged and ItemClick events for such purposes. The Gridview selections behaviour doesn't work, as once an item is selected it is never deselected. So finally, I've taken a different tack and am trying Tap and Right-Tap events for the Gridview Items. However the issue is, that no matter which way I approach it, I can't get Binding to work correctly.
So I have an object called LessonButton:
public class LessonButton : INotifyPropertyChanged
{
//public LessonButton() { }
public LessonButton(SolidColorBrush inBackground, bool inComplete, double inHeight, int inNumber, bool inSelected, bool inStarted,
Status inState, double inWidth)
{
...
Started = inStarted;
...
}
...
private bool _started;
public bool Started
{
get { return _started; }
set { if (_started != value) { _started = value; OnPropertyChanged(); } }
}
...
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
handler?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
It is added to an observable collection in the View Model:
public class LessonsViewModel : INotifyPropertyChanged
{
public ObservableCollection<LessonButton> Lessons { get; } = new ObservableCollection<LessonButton>();
private LessonButton _selectedLessonButton;
public LessonButton SelectedLessonButton
{
get { return _selectedLessonButton; }
set { if (_selectedLessonButton != value) { _selectedLessonButton = value; OnPropertyChanged(); } }
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
handler?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
In a user control I set the DataContext with:
<UserControl.DataContext>
<classes:LessonsViewModel/>
</UserControl.DataContext>
..and I then have a GridView defined as:
<GridView x:Name="LessonGridView" ItemContainerStyle="{StaticResource GridViewItemStyle}" ItemsSource="{Binding Lessons}"
SelectionMode="Single" IsItemClickEnabled="False" SelectedItem="{Binding Path=SelectedLessonButton, Mode=TwoWay}">
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<VariableSizedWrapGrid HorizontalChildrenAlignment="Left" MaximumRowsOrColumns="5" Orientation="Horizontal" VerticalChildrenAlignment="Top"/>
</ItemsPanelTemplate>
</GridView.ItemsPanel>
</GridView>
With the GridView item format defined in a ControlTemplate as part of the GridViewItemStyle.
I've tried to access the LessonButton variables in various ways using Binding and xBind, but could only get the program to run with the ControlTemplate using this XAML:
<Image Grid.Row="1" Grid.Column="1" Width="{StaticResource BadgeSize}"
Height="{StaticResource BadgeSize}" HorizontalAlignment="Right" VerticalAlignment="Top"
Opacity="{Binding Started, Converter={StaticResource OpacityConverterTrueValueIsVisible}}"
Source="/Assets/SelectionButtonGroup/square310x310logobw.png" Stretch="Uniform"/>
The Converter simply returns a 1 or 0 depending upon the value of the bool Started.
Although this code work, it is not correct somehow and Visual Studio reports an unknown error and states it cannot find the Started property. In fact it can't find any of the properties of LessonButton and I've been unable to find the correct syntax for exposing them, even with x:Bind code such as:
{x:Bind LessonViewModel.Lessons.LessonButton.Selected}
..or versions thereof, using casting etc.
I'm using Visual Studio 2017 Enterprise, which reports the aforementioned errors and displays wavy lines over the entire ControlTemplate with an error where it cannot find another Converter artefact that isn't even related to this code.. which in itself, I find extremely irritating. Is it me or does the XAML Intellisence in VS seem very flaky, in that it gives up and reports false errors if it can't identify the root cause of a real one?
Ideally I'd like the Gridview SelectedItem to bind with the ViewModel. But even trying actions via Tap events I can't get the binding to correctly expose LessonButton properties in the ControlTemplate XAML.
Any help would be greatly appreciated.
You shouldn't be using the ItemContainerStyle to Bind your LessonButton variables to. The ItemContainerStyle is used to style the Item with selection marks, its hover and pressed states etc.
You should instead use a DataTemplate stored inside your UserControl's resources like so:
<Grid>
<Grid.Resources>
<DataTemplate x:Name="GridViewTemplate">
<TextBlock Text="{Binding LessonName}">
</DataTemplate>
</StackPanel.Resources>
<GridView x:Name="GridView"
ItemsSource="{Binding Lessons}"
ItemTemplate="{StaticResource GridViewTemplate}">
</GridView>
</Grid>
Then give your DataTemplate a name (above "GridViewTemplate") and set it as the ItemTemplate of your GridView.
I am trying to bind the "IsChecked" property on the ToggleButton to "ModelView.IsEnabled".
"ModelView.IsEnabled" is always "false"
but somehow the ToggleButton can still show as "Checked".
Is there anything wrong with the binding?
XAML
...
<Page.Resources>
<ModelView:ModelView x:Key="ModelView"/>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<ToggleButton IsChecked="{Binding Source={StaticResource ModelView}, Path=IsEnabled, Mode=TwoWay}">
<TextBlock >UWP Toggle Button</TextBlock>
</ToggleButton>
</Grid>
...
ModelView.cs
using...
namespace App2
{
class ModelView : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public event EventHandler CanExecuteChanged;
private bool _isEnabled;
public bool IsEnabled
{
get {
return _isEnabled;
}
set
{
_isEnabled = false;
OnPropertyChanged("IsEnabled");
}
}
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
}
Try this, it worked to me:
1. Xaml code changes:
<Grid>
<Grid.DataContext>
<soHelpProject:MainViewModel/>
</Grid.DataContext>
<ToggleButton IsChecked="{Binding IsToggled, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}">
<TextBlock >UWP Toggle Button</TextBlock>
</ToggleButton>
</Grid>
regards,
In your class ModelView, change IsEnabled from this:
public bool IsEnabled
{
get {
return _isEnabled;
}
set
{
_isEnabled = false;
OnPropertyChanged("IsEnabled");
}
}
to this:
public bool IsEnabled
{
get {
return _isEnabled;
}
set
{
_isEnabled = value;
OnPropertyChanged("IsEnabled");
}
}
EDIT: If i use _isEnabled = !value; as you suggested, it still works, with button and state now showing opposite values:
EDIT 2: Now, if you want to properly test your binding, then you could add an extra regular button and do this:
private void button1_Click(object sender, RoutedEventArgs e)
{
myModelView.IsEnabled = !myModelView.IsEnabled;
}
so you can watch your ToggleButton switch between true and false every time you click Test Button. Please note that Test Button is not bound to anything, it's just for testing purposes. See corresponding XAML at the bottom.
The problem is that the way you're doing it, "forcing" IsEnabled to be always false, you're actually sabotaging your own code...:O)
And finally, it is not clear from your code when/where you're assigning your DataContext. Please see below how to do it.
XAML:
<Page.DataContext>
<local:MyModelView/>
</Page.DataContext>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<ToggleButton x:Name="toggleButton1" Content="ToggleButton" IsChecked="{Binding IsEnabled, Mode=TwoWay}" HorizontalAlignment="Center"/>
<TextBlock x:Name="textBlock1" Text="{Binding IsEnabled}" VerticalAlignment="Bottom" HorizontalAlignment="Center" Margin="126,0,201,286" />
<Button x:Name="button1" Click="button1_Click" Margin="127,400,0,220" Content="Test Button" Height="35" />
</Grid>
Code-behind:
private void Page_Loaded(object sender, RoutedEventArgs e)
{
myModelView = new MyModelView();
this.DataContext = myModelView;
}
I've run into the same problem, be it not with a ToggleButton, but with a TextBox, where I wanted to format the text the user had entered.
In your case you want to change the IsChecked property in your viewmodel and have it reflected in the User Interface straight away (so always be unchecked). The reason you want that is of absolutely no importance.
The problem is that with UWP the getter of your property gets called as you would expect when you click the ToggleButton. The normal action for the ToggleButton is to change from unchecked to checked (and vice versa) and that is what happens in your case. But then you expect that NotifyPropetyChanged signals the control in the UI. And that's where it goes wrong. The getter never gets called when the setter is executed (including NotifyPropertyChanged), so the UI doesn't reflect what you did in your setter.
This is very different from what the TwoWay Binding used to do (and still does in WPF). So there is nothing wrong with your code, but it seems that the binding mechanism has changed, although Microsoft claims it didn't. If you would use x:Bind, it works fine, so hat might solve your problem.
To clarify things more I have taken your example and modified it slightly, to show the problem.
I've put a ToggleButton on the page with a TwoWay binding to a viewmodel, exactly as you did. Clicking on the ToggleButton will switch its state from checked to unchecked and vice versa, even though the setter in my viewmodel Always sets the property to false (so unchecked).
But I've also added a normal button, that I've bound to a command that also modifies the property that the ToggleButton is bound to. Clicking this button calls the setter on the property the ToggleButton is bound to. Of course the setter gets called just the same, but after that the binding to the ToggleButton gets called, so NotifyPropertyChanged in this case does cause a UI update.
If you use the debugger, you can see exactly what i mean.
So your problem can be solved by using x:Bind, or by figuring out another way to update the UI, which you shouldn't have to do if Binding was still working as it used to. Maybe Microsoft has implemented some kind of optimization that now destroys classic Binding.
No special things, just a MainPage and a viewmodel.
My code for MainPage.xaml
<Page x:Class="App10.MainPage"
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:local="using:App10"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Page.Resources>
<local:ViewModel x:Key="viewModel" />
</Page.Resources>
<Grid x:Name="mainGrid" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel Margin="10,20,10,0">
<Button
x:Name="Button"
Content="UWP Normal button"
Command="{Binding Source={StaticResource viewModel}, Path=SwitchIschecked}"
HorizontalAlignment="Stretch" />
<ToggleButton
x:Name="toggleButton"
Margin="0,10,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
IsChecked="{Binding Source={StaticResource viewModel}, Path=IsChecked,
Mode=TwoWay}">
<TextBlock>UWP Toggle Button</TextBlock>
</ToggleButton>
</StackPanel>
</Grid>
</Page>
The code for MainPage.xaml.cs
using Windows.UI.Xaml.Controls;
// The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409
namespace App10
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
}
}
}
And the code for ViewModel.cs
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
namespace App10
{
public class ViewModel : INotifyPropertyChanged
{
private bool _isChecked;
// property for TwoWay binding with ToggleButton
public bool IsChecked
{
get
{
return _isChecked;
}
set
{
// extra var just to check 'value'
var _value = value;
// now always set it to false
_isChecked = false;
// Try to pass value of _isChecked to user interface
// because there is no check whether the value really
// has changed
// But this only works if the setter is not being called
// directly from the control the property is bound to
OnPropertyChanged();
}
}
private ICommand _switchChecked;
// ICommand for normal button, binding to Command
// calls method to set Property for ToggleButton
public ICommand SwitchIschecked
{
get
{
if ( _switchChecked == null )
_switchChecked = new ChangeChecked( new Action( ChangeVar ));
return _switchChecked;
}
set
{
_switchChecked = value;
}
}
// This will set the property for the ToggleButton
private void ChangeVar()
{
IsChecked = !IsChecked;
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged( [CallerMemberName] string propertyName = null )
{
var handler = PropertyChanged;
handler?.Invoke( this, new PropertyChangedEventArgs( propertyName ) );
}
}
/// <summary>
/// Quick class to implement ICommand
/// </summary>
class ChangeChecked : ICommand
{
Action _execute;
public ChangeChecked( Action execute )
{
_execute = execute;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute( object parameter )
{
return true;
}
public void Execute( object parameter )
{
_execute();
}
}
}
IsEnabled property is indicating whether the user can interact with the control. IsPressed is readonly property. So IsChecked is probably what you need.
My main page has the appbar and it is shared across different pages. I wrote the following code to open the appbar on the click of a gridview item.
XAML
<AppBar Opened="AppBar_Opened" IsOpen="{Binding IsAppBarOpen}">
Back end
private void Clock_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
App.ViewModel.SelectedClock = (Clock)ThemeGridView.SelectedItem;
App.WorldViewModel.IsAppBarOpen = true;
}
private void ThemeGridView_ItemClick(object sender, ItemClickEventArgs e)
{
App.ViewModel.SelectedClock = (Clock)ThemeGridView.SelectedItem;
App.WorldViewModel.IsAppBarOpen = true;
}
WorldViewModel
private bool _IsAppBarOpen;
public bool IsAppBarOpen
{
get { return _IsAppBarOpen; }
set { base.SetProperty(ref _IsAppBarOpen, value); }
}
GridView XAML
<GridView
Grid.Row="1"
Grid.Column="1"
x:Name="ThemeGridView"
ItemsSource="{Binding Clocks}"
ItemTemplate="{StaticResource WorldClockTemplate}"
SelectionChanged="Clock_SelectionChanged"
SelectionMode="None"
IsItemClickEnabled="True"
ItemClick="ThemeGridView_ItemClick"
>
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<WrapGrid />
</ItemsPanelTemplate>
</GridView.ItemsPanel>
</GridView>
But the appbar is not popping up when i select the gridview item. There is no binding error so its really mysterious!
There is not way to bind IsOpen property according the msdn:
Note Binding to the IsOpen property doesn't have the expected results
because the PropertyChanged notification doesn't occur when the
property is set.
<AppBar Opened="AppBar_Opened" IsOpen="{Binding IsAppBarOpen, **Mode=TwoWay**}">
This works for me. I use MVVM Light Toolkit.
public bool AppBarIsOpen
{
get { return this._appBarIsOpen; }
set
{
if (this._appBarIsOpen == value) { return; }
this._appBarIsOpen = value;
this.RaisePropertyChanged("AppBarIsOpen"); // without INotifyPropertyChanged it doesn't work
}
}
<AppBar
IsSticky="True"
IsOpen="{Binding Path=AppBarIsOpen, Mode=TwoWay}">
Roman Weisert's answer correctly states the likely reason for it not working, although you also must make the binding two-way as Zack Weiner suggested (I'm not sure the reason for the latter since the binding is not working in the target-to-source direction anyway). The current value of AppBar.IsOpen may not be reflected by IsAppBarOpen of your view-model. When that's the case, and you try updating the value, it's possible that no PropertyChanged event is raised since you may not actually be updating a value. Instead, you may be just setting the value from false to false or from true to true. Most SetProperty method implementations do not raise the PropertyChanged event unless there is an actual change, and I presume yours is the same.
To fix the problem, consider modifying your view-model as follows:
public bool IsAppBarOpen
{
get { return _IsAppBarOpen; } //changes initiated from UI not reflected
set //not updated from UI
{
_IsAppBarOpen = value;
base.OnPropertyChanged();
}
}
bool _IsAppBarOpen;
The notable difference from your view-model's code, is that SetProperty is not called here so PropertyChanged is raised even when the backing store equals the newly introduced value. In case your base class differs, note that mine has an OnPropertyChanged method with the signature
void OnPropertyChanged( [CallerMemberName] string propertyName = null )
that serves to raise the PropertyChanged event.
I can see from your use of the code-behind, though, that you are not really following MVVM. If MVVM is not a concern to you, then you could forgo the IsAppBarOpen property altogether and just directly set AppBar.IsOpen. As someone who religiously adheres to MVVM, however, I do not recommend that you further head in that (sinful) direction.
I had the same issue and using Caliburn Micro for WinRT and with this code worked for me:
<AppBar IsOpen="{Binding AppBarsOpen}" Name="MainAppBar" Padding="10,0,10,0" AutomationProperties.Name="Bottom App Bar">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50*" />
<ColumnDefinition Width="50*" />
</Grid.ColumnDefinitions>
<StackPanel x:Name="LeftPanel" Orientation="Horizontal" Grid.Column="0" HorizontalAlignment="Left">
<Button Name="ShowFlyout" Style="{StaticResource BookmarksAppBarButtonStyle}" />
</StackPanel>
<StackPanel x:Name="RightPanel" Orientation="Horizontal" Grid.Column="1" HorizontalAlignment="Right">
<Button Style="{StaticResource SaveAppBarButtonStyle}" />
</StackPanel>
</Grid>
</AppBar>
And that's your property in ViewModel:
public bool AppBarsOpen
{
get { return _appBarsOpen; }
set
{
if (value.Equals(_appBarsOpen)) return;
_appBarsOpen = value;
NotifyOfPropertyChange(() => AppBarsOpen);
}
}
Had the same issue, solved it by adding the Closed event and updating the ViewModel from the code behind. Saw no other way since TwoWay binding was not working as Roman pointed out.
XAML
<AppBar x:Name="BottomAppBar1"
AutomationProperties.Name="Bottom App Bar"
Closed="BottomAppBar1_Closed"
IsOpen="{Binding IsOpen, Mode=TwoWay}"
IsSticky="True">
C# Code behind
private void BottomAppBar1_Closed(object sender, object e)
{
MainViewModel vm = this.DataContext as MainViewModel;
vm.IsOpen = false;
}
C# MainViewModel
public const string IsOpenPropertyName = "IsOpen";
private bool isOpen = false;
/// <summary>
/// Sets and gets the IsOpen property.
/// Changes to that property's value raise the PropertyChanged event.
/// </summary>
public bool IsOpen
{
get
{
return isOpen;
}
set
{
RaisePropertyChanging(IsOpenPropertyName);
isOpen = value;
RaisePropertyChanged(IsOpenPropertyName);
}
}
You should bind both IsOpen and IsSticky two way because otherwise you will get problems with for example having to tap two time to unselect an item (once to close the app bar and once for unselecting) and also it's the will help having your app bar behave more standarly (will prevent the app bar to pop down on tap when an item is selected).
To show the app bar you will need to do the following (the order is important):
this.IsAppBarSticky = true;
this.IsAppBarOpen = true;
and to hide it, do the following:
this.IsAppBarSticky = false;
this.IsAppBarOpen = false;
Another way to make this work without having to use a codebehind handler for app bar closed event:
public class AppBarClosedCommand
{
public static readonly DependencyProperty CommandProperty = DependencyProperty.RegisterAttached("Command", typeof(ICommand),
typeof(AppBarClosedCommand), new PropertyMetadata(null, CommandPropertyChanged));
public static void SetCommand(DependencyObject attached, ICommand value)
{
attached.SetValue(CommandProperty, value);
}
public static ICommand GetCommand(DependencyObject attached)
{
return (ICommand)attached.GetValue(CommandProperty);
}
private static void CommandPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// Attach click handler
(d as AppBar).Closed += AppBar_onClose;
}
private static void AppBar_onClose(object sender, object e)
{
// Get GridView
var appBar = (sender as AppBar);
// Get command
ICommand command = GetCommand(appBar);
// Execute command
command.Execute(e);
}
}
then in the XAML you can use it like :
common:AppBarClosedCommand.Command="{Binding AppBarClosedCommand}"
with the command function looking like:
public void OnAppBarClosed()
{
AppBarOpen = false;
}
Im all new in the world of C# and .net platform,so please be easy on me.
This forum helped me in a few problems that i came into while doing my project,but im now stuck on this for a few days.
What i'm trying to achieve is to set the selecteditem of a combobox by passing a string to it.
The scenario is :
I have a datatable and im setting the combo's itemssource to that datatable.DefaultView.
Also i set the DisplayMemberPath of the combo,and so far everything is ok,the items show up in the combobox.
Beside this i have a string with some value that i have inside the combobox too.
So i'm trying to set the selecteditem of the combo like this :
combo.SelectedItem = mystring;
As you can guess,it's not working. Strangely,when i do this:
combo.Items.Add(mystring);
combo.SelectedItem = mystring;
It's working. So this is why I'm confused!
EDIT:
I just found the solution :
combo.ItemsSource = datatable.DefaultView;
combo.DisplayMemberPath = "yourpath";
combo.SelectedValuePath = "yourpath";
combo.SelectedValue = mystring;
So the trick was to set the SelectedValuePath and the SelectedValue properties.
I don't know is this a good programming practice,but this does exactly what i needed.
You're doing something wrong.
Here's a demo app that shows this (the project should be named "StringCombo").
<Window
x:Class="StringCombo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
ResizeMode="CanResize">
<Window.DataContext>
<ViewModel
xmlns="clr-namespace:StringCombo" />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ComboBox
Name="OldeFashonedCombo" />
<Button
Grid.Column="1"
Content="Select Olde Waye"
Click="Button_Click" />
<ComboBox
Grid.Row="1"
ItemsSource="{Binding Strings}"
SelectedItem="{Binding SelectedString}" />
<Button
Grid.Row="1"
Grid.Column="1"
Content="Select New Way"
Command="{Binding SelectString}" />
</Grid>
</Window>
We've got two combos and two buttons. One uses the old winforms method of codebehind to manipulate the combo, and the other uses the new MVVM pattern.
In both scenarios, the user clicks the button, it sets the combo's SelectedValue, and the combo updates on the ui.
Here's the codebehind version:
public MainWindow()
{
InitializeComponent();
OldeFashonedCombo.Items.Add("One");
OldeFashonedCombo.Items.Add("Two");
OldeFashonedCombo.Items.Add("Three");
}
private void Button_Click(object sender, RoutedEventArgs e)
{
OldeFashonedCombo.SelectedItem = "Two";
}
Notice I'm not using the same "instance" of "Two"; there is no need as strings are "interned," or the same instance is automatically reused, in the .NET platform. object.ReferenceEquals("Two","Two") is always true.
So, I add strings to the Items collection, and when the button is clicked I set the SelectedItem to "Two". SelectedItem is the actual instance within the Items collection that should be selected. SelectedValue is the display value; you can select by this IIRC, but I wouldn't do that as a best practice.
Here's the MVVM version:
public sealed class ViewModel : INotifyPropertyChanged
{
public ObservableCollection<string> Strings { get; private set; }
public ICommand SelectString { get; private set; }
public string SelectedString { get; set; }
public ViewModel()
{
Strings = new ObservableCollection<string>();
Strings.Add("Foo");
Strings.Add("Bar");
Strings.Add("Baz");
SelectString = new SelectStringCommand
{
ExecuteCalled = SelectBar
};
}
private void SelectBar()
{
SelectedString = "Bar";
// bad practice in general, but this is just an example
PropertyChanged(this, new PropertyChangedEventArgs("SelectedString"));
}
public event PropertyChangedEventHandler PropertyChanged;
}
/// <summary>
/// ICommands connect the UI to the view model via the commanding pattern
/// </summary>
public sealed class SelectStringCommand : ICommand
{
public Action ExecuteCalled { get; set; }
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
ExecuteCalled();
}
}
Again, because of interning, I do not have to use the same "instance" of the string. To see how the ViewModel connects to the UI, check the bindings on the ComboBox and the Button (If you haven't looked into it yet, I'd strongly suggest ditching codebehind for MVVM. It may take a little more effort to figure it out, but its MUCH better in the long run).
ANYHOW, if you run this app you'd see that BOTH versions work as expected. When you click the button, the combo box is updated properly. This suggests that your code is wrong in some other way. Not sure what, as you haven't given us enough detail to determine this. But if you run the sample and compare it closely with your code, you might be able to figure this out.
I think using the findby will work so something like
combo.ClearSelection();
combo.Items.FindByValue(mystring).Selected = true;