UWP PivotItem Header visibility binding - c#

I'm doing a binding in UWP, the listview is not showing(so it's working fine), but the header is still on the view.
So the problem is that the header is not getting the binding from the PivotItem, what can be the problem?
<PivotItem Header="Hello" Visibility="{Binding isVisible, Converter={StaticResource Visibility}}">
<ListView ItemsSource="{Binding myList}"/>
</PivotItem>

This is actually a bit tricky. Setting Visibility on a PivotItem does indeed hide only the contents of the item, not the PivotItem itself. That said, you can hide it from the code-behind by removing it from the pivot completely:
MyPivot.Items.Remove(HideablePivotItem);
The problem is now the fact that you need to trigger it on binding change. For that purpose I suggest you use a custom Behavior and a CallMethodAction.
First install the Microsoft.Xaml.Behaviors.Uwp.Managed from NuGet (right-click your project, click Manage NuGet Packages... find the package using search and click Install.
Now, create a new class DataChangeTriggerBehavior class:
public class DataChangeTriggerBehavior : Trigger<FrameworkElement>
{
public static readonly DependencyProperty BindingProperty = DependencyProperty.Register(
nameof(Binding), typeof(object), typeof(DataChangeTriggerBehavior), new PropertyMetadata(null, BindingChanged));
public object Binding
{
get => (object)GetValue(BindingProperty);
set => SetValue(BindingProperty, value);
}
private static void BindingChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
{
DataChangeTriggerBehavior changeTrigger = (DataChangeTriggerBehavior)dependencyObject;
if (changeTrigger.AssociatedObject == null) return;
Interaction.ExecuteActions(changeTrigger.AssociatedObject, changeTrigger.Actions, args);
}
}
This behavior will observe a binding and trigger its associated actions whenever the binding changes.
Now, update your Page element as follows:
<Page
...
x:Name="Page"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:core="using:Microsoft.Xaml.Interactions.Core"
xmlns:customBehavior="using:XXX"
mc:Ignorable="d">
Where XXX is the namespace where your behavior is defined.
Now use the behavior in your Pivot:
<Pivot x:Name="MyPivot">
<interactivity:Interaction.Behaviors>
<local:DataChangeTriggerBehavior Binding="{Binding isVisible}">
<core:CallMethodAction MethodName="TogglePivotItem"
TargetObject="{Binding ElementName=Page}" />
</local:DataChangeTriggerBehavior>
</interactivity:Interaction.Behaviors>
<PivotItem Header="Hello" Visibility="Collapsed" x:Name="HideablePivotItem">
<ListView ItemsSource="{Binding myList}"/>
</PivotItem>
</Pivot>
Finally you must define the TogglePivotItem method in your page's code-behind:
private int originalPosition = 0;
public void TogglePivotItem()
{
if (MyPivot.Items.Contains(HideablePivotItem))
{
//store the position of the item to be readded later
originalPosition = MyPivot.Items.IndexOf(HideablePivotItem);
MyPivot.Items.Remove(HideablePivotItem);
}
else
{
MyPivot.Items.Insert(originalPosition, HideablePivotItem);
}
}
I am storing the original position of the PivotItem so that it can be re-added to the same place again.

Related

C# & wpf - Unexpected behavior of (OneWay-Mode) chain-binding between ListBox-Label-ComboBox

I have the following strange (for me) situation
A ListBox is bound (as Source) to a Label with OneWay Mode, i.e. ListBox is read-only.
The Label is then bound to a ComboBox with TwoWay binding
ListBox --> Label <--> ComboBox - arrows denote binding mode
The strange thing is that when the program starts and the user selects through the list in the ListBox, all 3 controls behave as expected.
But as soon as one index is chosen from Combobox, the Label continues to work properly (is updated by the Combo), but the OneWay binding to ListBox disappears (is null) and the ListBox cannot update the Label any more.
It seems to me that when Label Content is set by other means besides the OneWay binding (as here with the Combo updating or maybe with a ValueConverter), this binding is cleared by WPF.
The other strange behavior is that if this OneWay binding between ListBox and Label is turned into a TwoWay one, then everything works perfectly.
The question is what am I doing wrong, or if this is the normal behavior, where could I find relevant documentation.
Please find below simplified code and XAML demonstrating the case.
My workaround is to set the Label Content with code in ListBox_SelectionChanged.
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace Test_Chained_controls
{
public partial class MainWindow : Window
{
public class ComboItems
{
public int iDX { get; set; }
public string sDesc { get; set; }
public ComboItems(int a, string b)
{
iDX = a;
sDesc = b;
}
}
public class ListItems
{
public int iLDX { get; set; }
public ListItems(int a)
{
iLDX = a;
}
}
public List<ListItems> intList = new List<ListItems>();
public List<ComboItems> idx_StrList = new List<ComboItems>();
public MainWindow()
{
InitializeComponent();
intList.Add(new ListItems(0));
intList.Add(new ListItems(1));
intList.Add(new ListItems(2));
intList.Add(new ListItems(3));
idx_StrList.Add(new ComboItems(0, "Zero"));
idx_StrList.Add(new ComboItems(1, "One"));
idx_StrList.Add(new ComboItems(2, "Two"));
idx_StrList.Add(new ComboItems(3, "Three"));
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
listBox.ItemsSource = intList;
comboBox.ItemsSource = idx_StrList;
}
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
//// Set Label Content in case of OneWay
// var binding = BindingOperations.GetBinding(label, Label.ContentProperty);
// if (binding != null)
// {
// if (binding.Mode == BindingMode.OneWay)
// {} // Binding set - do nothing
// }
// else label.Content = listBox.SelectedItem;
}
}
}
XAML
<Window ... normal stuff
xmlns:local="clr-namespace:Test_Chained_controls"
mc:Ignorable="d"
Title="MainWindow" Height="182" Width="500" Loaded="Window_Loaded">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="140"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition Height="100"/>
</Grid.RowDefinitions>
<Label Content="ListBox" Grid.Row="0" Grid.Column="0" Margin="20,10,0,0" />
<Label Content="Label" Grid.Row="0" Grid.Column="1" Margin="20,10,0,0" />
<Label Content="ComboBox" Grid.Row="0" Grid.Column="2" Margin="20,10,0,0" />
<ListBox x:Name="listBox" Grid.Row="1" Grid.Column="0" Margin="0"
DisplayMemberPath="iLDX"
SelectedIndex="0"
IsSynchronizedWithCurrentItem="True"
SelectionChanged="ListBox_SelectionChanged"/>
<Border BorderThickness="1" Grid.Row="1" Grid.Column="1" Height="30"
Margin="20,20,0,0" BorderBrush="#FFACACAC" >
<!-- *********** Label with Mode=OneWay or TwoWay *********** -->
<Label x:Name="label" Width="100" Height="25"
Content="{Binding ElementName=listBox,
Path=SelectedItem.iLDX, Mode=OneWay }" />
</Border>
<ComboBox x:Name="comboBox" Grid.Row="1" Grid.Column="2"
Height="30" Margin="20,20,0,0"
DisplayMemberPath="sDesc"
SelectedValue="{Binding ElementName=label, Path=Content,
TargetNullValue=0, FallbackValue=0, Mode=TwoWay}"
SelectedValuePath="iDX" />
</Grid>
</Window>
EDIT
Relevant documentation: Dependency properties overview
Local value: A local value might be set through the convenience of the property wrapper, which also equates to setting as an attribute or property element in XAML, or by a call to the SetValue method using a property of a specific instance. If you set a local value by using a binding or a static resource, these each act in the precedence as if a local value was set, and bindings or resource references are erased if a new local value is set.
and further down
If you set another local value for a property that originally held a Binding value, you will overwrite the binding entirely, not just the binding's run-time value.
As I understand, there was some kind of bug related to this case, fixed with the introduction of DependencyObject.SetCurrentValue The Control Local Values Bug Solution
public void SetCurrentValue (System.Windows.DependencyProperty dp, object value);
// Sets the value of a dependency property without changing its value source.
It seems to me that Combobox TwoWay binding is still using SetValue, and that's why the binding for (label) gets erased when my (combobox) is used.
To overcome this, I changed the TwoWay binding of (comboBox) to OneWay, and entered the following in the comboBox_DropDownClosed event (showing the currently selected Item), in order to update (label) by code without erasing the existing binding
private void comboBox_DropDownClosed(object sender, System.EventArgs e)
{
Binding binding = BindingOperations.GetBinding(label, Label.ContentProperty);
if (binding != null)
{
ComboItems ComboItem = comboBox.SelectedItem as ComboItems;
int iDX = ComboItem.iDX;
// Set label value without affecting existing binding
label.SetCurrentValue(Label.ContentProperty, iDX);
}
}
With the use of SetCurrentValue, code works now as originally intended by "simulating" the TwoWay mode.
There's nothing strange at all. Data binding is designed to work this way. When you assign a binding to a dependency property, it means you change the local value of this dependency property to a binding expression. And any update provide by the binding source will be the effective value of this dependency property. If the binding is working in one way mode, any update to this dependency property from other then binding source, will overwrite the local value, result in losing the binding. On the other side, becuase two mode is suppose to update the binding source, dependency object will count any non-expression value as effective value, binding will keep working until you replace or clear it.
DependencyObject.GetValue gets the effective value.
DependencyObject.ReadLocalValue gets the local value.
DependencyObject.SetValue sets the local value.
DependencyObject.SetCurrentValue sets the effective value.
DependencyObject.ClearValue clears the local value.

ListView not updating on CollectionChanged

I am starting to play with Realm, and I am trying to bind a collection from the Realm database to a ListView. The binding works fine, but my ListView does not update when adding new items. My understanding is that IRealmCollection<> implements INotifyCollectionChanged and INotifyPropertyChanged events.
Here is a simple application to reproduce the issue:
View:
<Page x:Class="App3.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:App3"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel>
<Button Click="ButtonBase_OnClick" Content="Add" />
<ListView x:Name="ListView">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Id}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</Grid>
</Page>
CodeBehind:
namespace App3
{
public class Thing : RealmObject
{
public string Id { get; set; } = Guid.NewGuid().ToString();
}
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{
private Realm _realm;
private IRealmCollection<Thing> things;
public MainPage()
{
this.InitializeComponent();
_realm = Realm.GetInstance();
things = (IRealmCollection<Thing>)_realm.All<Thing>();
ListView.ItemsSource = things;
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
_realm.Write(() =>
{
var thing = new Thing();
_realm.Add(thing);
});
}
}
}
I normally use MVVM (Template10), but this is a simple application to demonstrate the issue. Clicking the Add button adds an item to the database, but the ListView only updates when the application is first loaded. I have read similar questions, but I have not been able to find an answer that works yet. Inverse Relationships and UI-Update not working? is the closest I have found yet, but still does not fix the issue.
EDIT
I can force it to rebind like so:
ListView.ItemsSource = null;
ListView.ItemsSource = things;
But that is not optimal. I am trying to take advantage of Realm's "live objects" where the collection should always know when items are changed or added.
EDIT 2
Setting BindingMode=OneWay in code-behind also does not change the behavior:
_realm = Realm.GetInstance();
things = (IRealmCollection<Thing>)_realm.All<Thing>();
var binding = new Binding
{
Source = things,
Mode = BindingMode.OneWay
};
ListView.SetBinding(ListView.ItemsSourceProperty, binding);
SOLUTION
It turned out to be a known issue in IRealmCollection: https://github.com/realm/realm-dotnet/issues/1461#issuecomment-312489046 which is fixed in Realm 1.6.0. I have updated to the pre-release NuGet package and can confirm that the ListView now updates as expected.
Set Mode=OneWay in Binding
Method 1: In Xaml
<ListView ItemsSource="{x:Bind things, Mode=OneWay}" />
Method 2: In Code Behind
Binding myBind = new Binding();
myBind.Source = things;
myBind.Mode = BindingMode.OneWay;
myListView.SetBinding(ListView.ItemsSourceProperty, myBind);
It is a bug in IRealmCollection. You can use Prerelease Nuget to solve it.
For more info:
IRealmCollection does not update UWP ListView
GitHub Issue: IRealmCollection does not update UWP ListView

Keep a reference to objects passed to a UserControl

I created a UserControl that has a ContentControl in it. This ContentControl gets Buttons from the normal .xaml-pages. But depending on some events I need to change this Button's Label or Image but i am getting a NullReferenceException.
UserControl1.xaml
<Grid>
<!-- different Stuff that needs to be around -->
<ContentControl Content="{Binding UserControlContent, ElementName=userContent}"/>
</Grid>
UserControl1.xaml.cs
public static readonly DependencyProperty AppBarContentProperty =
DependencyProperty.Register("UserControlContent", typeof(Grid), typeof(UserControl1), new PropertyMetadata(new Grid()));
public Grid UserControlContent
{
get { return (Grid)GetValue(UserControlContentProperty); }
set { SetValue(UserControlContentProperty, value); }
}
MainPage.xaml
<local:UserControl1>
<local:UserControl1.UserControlContent>
<Grid>
<Controls:RoundButton x:Name="btn1"/>
</Grid>
</local:UserControl1.UserControlContent>
</local:UserControl1>
MainPage.xaml.cs
MainPage()
{
btn1.Label = "new label";
}
As soon as I try this with a button inside of the UserControl it fails. With buttons that stay outside it works.
Is there any deeper binding possible to keep control of these buttons?
The trick is using the mvvm-binding!
The button's values are bound now:
Label="{Binding RoundButtons[3].Label}"
Visibility="{Binding RoundButtons[3].VisibilityState, FallbackValue=Visible}"
This allows me to define default-values and still change them on the fly as I need them to be changed.
Hope someone needs this information ;)

WP7.8: Bound items in scrollbox updated with wrong data

Overview
I have an application, that displays data from an observable collection. The observable collection is (in this debugging setting) created and instanciated only once, then the values stay the same.
The main view of the application contains a ListBox that is bound to said observable collection:
<ListBox x:Name="MainListBox" ItemsSource="{Binding Items}" SelectionChanged="MainListBox_SelectionChanged" >
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel MinWidth="456" MaxWidth="456" Background="White" Margin="0,0,0,17">
<sparklrControls:SparklrText Post="{Binding Path=.}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
<!-- Workaround used to stretch the child elements to the full width -> HorizontalContentAlignment won't work for some reason... -->
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"></Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
The child items are bound to a UserControl. This UserControl implements a DependancyProperty which the child elements are bound to:
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(object), new PropertyMetadata(textPropertyChanged));
private static void postPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SparklrText control = d as SparklrText;
control.Post = (ItemViewModel)e.NewValue;
}
Binding to the post property configures other variables via the getter of the Post property
public ItemViewModel Post
{
get
{
return post;
}
set
{
if (post != value)
{
this.ImageLocation = value.ImageUrl;
this.Username = value.From;
this.Comments = value.CommentCount;
this.Likes = value.LikesCount;
this.Text = value.Message;
post = value;
}
}
}
This setter configures other which in turn set up elements in the user control. Nothing in the user control is bound, the few updates are done with direct access to the respective Content/Text properties. ImageLocation performs an asynchronous download of an image with
private void loadImage(string value)
{
WebClient wc = new WebClient();
wc.OpenReadCompleted += (sender, e) =>
{
image = new BitmapImage();
image.SetSource(e.Result);
MessageImage.Source = image;
};
wc.OpenReadAsync(new Uri(value));
}
Issue
When I scroll down in the list box and back up, the setter of Post is executed when the owning element comes back into view. The problem: value is a different instance of ItemViewModel. The ListBox ItemsSource is not accessed in any way from outside the class. When scrolling back up, it seems like the wrong Items are bound to the elements, resulting in distorted designs. Are there any issues with the Binding that cause this?
The issue was caused by the ListBox. Elements that are scroll out of view are recycled and appended on the other side. In the code above, a asynchronous operation did not check if the result was still valid, causing wrong display data.

DataGrids over ScrollViewer prevent it to scroll

I have multiples DataGrids disposed over a ScrollViewer.
These DataGrids have an "height: auto" property so that I can hide the scrollbar and view all the content.
The only problem is that the DataGrids takes the focus and so I can't scroll the ScrollViewer.
Is that a property to keep the focus on the ScrollViewer but also keeping the behaviour of the DataGrids (so I can select elements) ?
Thank you !
It's to late, but I resolved this problem in this manner:
I created the PreviewMouseWheel event for DataGrid
and manually scrolled the wrapping ScrollViewer
private void dgInvoicesItems_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
this.scrInvoice.ScrollToVerticalOffset(this.scrInvoice.ContentVerticalOffset - e.Delta);
}
I ran across this exact same issue except my scenario was a little bit more complicated. Instead of having DataGrid in a ScrollViewer, I had a bunch of UserControl (called ProductDataGrid and defined below) in my ScrollViewer:
ProductDataGrid.xaml:
<UserControl x:Class="My.Control.ProductDataGrid" ...>
<Grid>
<Grid.RowDefinitions>...</Grid.RowDefinitions>
<TextBlock x:Name="Header" Grid.Row="0" ... />
<DataGrid x:Name="ProductData" Grid.Row="1" ... />
</Grid>
</UserControl>
ProductPortfolioListView.xaml:
<Page ...
xmlns:my="clr-namespace:My.Control"
....>
<Grid>
<Grid.RowDefinitions>...</Grid.RowDefinitions>
<ScrollViewer x:Name="ProductScrollViewer">
<StackPanel>
<my:ProductDataGrid ... />
<my:ProductDataGrid ... />
<my:ProductDataGrid ... />
</StackPanel>
</ScrollViewer>
The solution provided by Livsi is spot on correct but my UserControl did not have access to my ScrollViewer, so here is my solution:
ProductPortfolioListView.xaml:
<Page ...
xmlns:my="clr-namespace:My.Control"
....>
<Grid>
<Grid.RowDefinitions>...</Grid.RowDefinitions>
<ScrollViewer x:Name="ProductScrollViewer">
<StackPanel>
<my:ProductDataGrid ...
PreviewMouseWheel="ProductDataGrid_PreviewMouseWheel" />
<my:ProductDataGrid ...
PreviewMouseWheel="ProductDataGrid_PreviewMouseWheel" />
<my:ProductDataGrid ...
PreviewMouseWheel="ProductDataGrid_PreviewMouseWheel" />
</StackPanel>
</ScrollViewer>
ProductPortfolioListView.xaml.cs:
void ProductDataGrid_PreviewMouseWheel(object sender, MouseWheelEventArgs args)
{
ProductScrollViewer.ScrollToVerticalOffset(ProductScrollViewer.ContentVerticalOffset - args.Delta;
args.Handled = true;
}
Note the beauty of this solution lies in the fact that I can separate my DataGrid from the Page that will hold them, so I achieve code isolation as well as less duplicated code. And even better, I absolutely utilize the fact that RoutedEvents keep propragating from the Source to all of its parents until someone handles it (which in my case is my ProductScrollViewer).
TopMouseScrollPriorityBehavior.TopMouseScrollPriority
You can simply set the following Attached Property to your ScrollViewer
public class TopMouseScrollPriorityBehavior
{
public static bool GetTopMouseScrollPriority(DependencyObject obj)
{
return (bool)obj.GetValue(TopMouseScrollPriorityProperty);
}
public static void SetTopMouseScrollPriority(DependencyObject obj, bool value)
{
obj.SetValue(TopMouseScrollPriorityProperty, value);
}
public static readonly DependencyProperty TopMouseScrollPriorityProperty =
DependencyProperty.RegisterAttached("TopMouseScrollPriority", typeof(bool), typeof(TopMouseScrollPriorityBehavior), new PropertyMetadata(false, OnPropertyChanged));
private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var scrollViewer = d as ScrollViewer;
if (scrollViewer == null)
throw new InvalidOperationException($"{nameof(TopMouseScrollPriorityBehavior)}.{nameof(TopMouseScrollPriorityProperty)} can only be applied to controls of type {nameof(ScrollViewer)}");
if (e.NewValue == e.OldValue)
return;
if ((bool)e.NewValue)
scrollViewer.PreviewMouseWheel += ScrollViewer_PreviewMouseWheel;
else
scrollViewer.PreviewMouseWheel -= ScrollViewer_PreviewMouseWheel;
}
private static void ScrollViewer_PreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
{
var scrollViewer = (ScrollViewer)sender;
scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - e.Delta);
e.Handled = true;
}
}
Usage
<ScrollViewer b:TopMouseScrollPriorityBehavior.TopMouseScrollPriority="True" VerticalScrollBarVisibility="Auto" Margin="5" PanningMode="VerticalFirst">
<DataGrid ScrollViewer.PanningMode="None" ItemsSource="{Binding Items}" />
</ScrollViewer>
Where b: is the namespace that contains this behavior
Touch Support
To enable touch support you might also want to set ScrollViewer.PanningMode to None on your DataGrid and set the same property to VerticalFirst or other value on your top level ScrollViewer
Example
<ScrollViewer VerticalScrollBarVisibility="Auto" Margin="5" PanningMode="VerticalFirst">
<DataGrid ScrollViewer.PanningMode="None" ItemsSource="{Binding Items}" />
</ScrollViewer>
Try setting the CanContentScroll on the DataGrid to False like this:
<DataGrid ScrollViewer.CanContentScroll="False" ... />

Categories