I just started developing with Xamarin Forms and facing a rather simple problem, but I could not find a solution:
I have some UI elements stacked over each other like the following:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label x:Name="LabelLeft" Grid.Column="0" Text="LEFT" />
<Label x:Name="LabelRight" Grid.Column="2" Text="RIGHT" />
<Grid x:Name="GridMain" Grid.Column="0" Grid.ColumnSpan="3" Margin="0"
BackgroundColor="Default">
<Grid.GestureRecognizers>
<PanGestureRecognizer PanUpdated="PanGestureRecognizer_OnPanUpdated"/>
</Grid.GestureRecognizers>
<Grid Margin="10">
<Label Text="MAIN"/>
</Grid>
</Grid>
</Grid>
The GridMain can then be "panned away" to "show" the two labels.
But now I have the problem that the grids background color Default is not the target systems default background color, but Transparent -> I can "see" the labels underneath the grid. Also I can only pan the grid when I click/touch the string inside the grid and not anywhere inside the grid.
How can I set the background to use the "non-transparent" default background?
I do not want to use hard-coded White as I also want to support e.g. the dark theme for UWP and there it should be Black.
For now I used Accent to continue working, but that is not what I actually want.
As a workaround you could create a dependency service that would return the color you want to use for the given OS.
First create a IColorService interface in the shared project:
public interface IColorService
{
Color SystemBackgroundColor { get; }
}
Then implement it on each platform:
[assembly: Xamarin.Forms.Dependency(typeof(ColorService))]
namespace App.UWP
{
public class ColorService : IColorService
{
public Color SystemBackgroundColor =>
Application.Current.RequestedTheme == ApplicationTheme.Dark ?
Color.Black : Color.White;
}
}
Next, create a static class in the shared project, that will allow easy access to the color:
public static class Colors
{
public static Color SystemBackgroundColor =>
DependencyService.Get<IColorService>().SystemBackgroundColor;
}
Finally use the color as the BackgroundColor of your Grid:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:App"
x:Class="App.MainPage">
<Grid BackgroundColor="{x:Static local:Colors.SystemBackgroundColor}" />
</Grid>
</ContentPage>
Notice the added xmlns:local pointing to the namespace where the Colors class is defined.
Related
I am trying to use a ParallaxView to make an image within the GridViewItem parallax as the GridView is scrolled. The intended effect is the same as the newsfeed in the Xbox UWP app on PC; images on the listview items there parallax as you scroll. This is shown visually in the following image:
I am running into a databinding data context issue, however. All of the examples I have seen are to make the background of the entire GridView or ListView parallax. A working example of that is as follows (very similar to the XAML Controls Gallery Sample found here):
<Grid>
<ParallaxView Name="GridViewParallaxView"
Source="{x:Bind MyGridView}"
VerticalShift="100">
<!-- This is the background image that parallaxes. -->
<Image></Image>
</ParallaxView>
<GridView Name="MyGridView">
<!-- GridView Content Here... -->
</GridView>
</Grid>
The problem I am running into is when trying to place the ParallaxView INSIDE of the DataTemplate in the ItemTemplate in the GridView.
<GridView Name="MyGridView"
ItemsSource="{x:Bind MyDataList}">
<GridView.ItemTemplate>
<DataTemplate x:DataType="models:MyDataType">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<ParallaxView Name="GridViewParallaxView"
Grid.Row="0"
Source="{x:Bind MyGridView}"
VerticalShift="100">
<!-- This is the image ON EACH GRIDVIEW ITEM that parallaxes. -->
<Image Source="{x:Bind MySource}"></Image>
</ParallaxView>
<TextBlock Name="ItemTitleTextBlock"
Grid.Row="1"
Text="{x:Bind Title}"></TextBlock>
</Grid>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
Note that some properties (like MinWidth and Margin) have been omitted for brevity.
The code behind (not totally relevant, but to add context to some of the bindings):
public class MyPage : Page
{
public ObservableCollection<MyDataType> MyDataList { get; set; }
}
public class MyDataType
{
public string Title { get; set; }
public ImageSource MySource { get; set; }
}
This does not work because placing the 'ParallaxView' inside of the 'DataTemplate' changes the 'DataContext'. MyGridView can no longer be bound to directly like that. So how do I bind it?
Also, I read that the DataContext property is inherited by children in the XAML tree. I need the Image databinding to be in the same context as the DataTemplate. Is there a way to just change the DataContext for the ParallaxView?
You could use Binding, instead of x:Bind.
<ParallaxView Name="GridViewParallaxView"
Grid.Row="0"
Source="{Binding ElementName=MyGridView}"
VerticalShift="100">
<!-- This is the image ON EACH GRIDVIEW ITEM that parallaxes. -->
<Image Source="{x:Bind MySource}"></Image>
</ParallaxView>
Problem
This is the layout of my main pane:
<Page
x:Class="Communities.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Communities"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" Loaded="Page_Loaded" DataContext="{Binding RelativeSource={RelativeSource Mode=Self}}">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="48" />
<RowDefinition Height="*" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
...
<SplitView Grid.Row="1" Name="hamburgerMenu" OpenPaneLength="200" PaneBackground="#F02A2A2A">
<SplitView.Pane>
<ListView ItemsSource="{Binding}" IsItemClickEnabled="True" ItemClick="HamburgerItemClick">
... </ListView>
</SplitView.Pane>
<Frame Name="frame" />
</SplitView>
<Grid Grid.RowSpan="3" Name="popupArea" />
</Grid>
</Page>
the frame is where I load all my pages, so that the layout is always consistant.
Now, in most of my child pages I have defined AppBar control and attached it to the BottomAppBar property of that child page:
PostView.xaml
...
<Page.BottomAppBar>
<CommandBar>
<AppBarButton Label="Back" Icon="Back" Click="TryGoBack" />
<AppBarButton Label="Refresh" Icon="Refresh" Click="TryRefreshComments" />
<AppBarButton Label="Go to Community" Icon="Go" Click="TryOpenCommunity" />
</CommandBar>
</Page.BottomAppBar>
...
Here's where the trouble starts. It works fine on PC, as the layout is mostly static on desktop. There are no software keyboards required most of the time etc. On mobile it's more problematic: Screenshots
My thoughts
It seems like the frame that is used to display the child page is causing all sorts of problems. When the AppBar is defined in the main page it positions correctly.
I'd like to avoid the keyboard covering the textbox as well as the AppBar but I don't want to get rid of the frame control. I'd also prefer it if the page got "squished" when the keyboard shows up, instead of getting pushed upwards, but I'm not sure how to display the keyboard on the frame level, instead of the entire MainPage, default level.
What would be the best way to solve this situation?
Cheers!
As you know, if we set the Page.BottomAppBar in the root of the Page, there is no issue with Touch keyboard. It seems it is the best way to add the Page.BottomAppBar.
If you want to add the Page.BottomAppBar in the other page in the Frame, you should be able to customize your UI. The UWP provides similar behavior on the appearance of the touch keyboard by handling the Showing and Hiding events exposed by the InputPane object.
We can use the InputPaneVisibilityEventArgs.OccludedRect to get the region of the application's window that the input pane is covering.
For example:
public PostView()
{
this.InitializeComponent();
InputPane.GetForCurrentView().Showing += PostView_Showing;
InputPane.GetForCurrentView().Hiding += PostView_Hiding;
}
private void PostView_Hiding(InputPane sender, InputPaneVisibilityEventArgs args)
{
MyTextBox.Margin = new Thickness(0, args.OccludedRect.Height, 0, 0);
}
private void PostView_Showing(InputPane sender, InputPaneVisibilityEventArgs args)
{
MyTextBox.Margin = new Thickness(0, 0, 0, args.OccludedRect.Height);
}
I've got a shared Flyout defined in my <Page.Resources> as follows:
<Flyout x:Name="InfoFlyout" Opened="{Binding IsOpen,
ElementName=MyListView, Mode=TwoWay}">
<Grid>
<Button Foreground="White" Margin="5">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Help"/>
</StackPanel>
</Button>
</Grid>
</Flyout>
But I get An object reference not set error when compiling, so I used the code from this article (Using Windows 8.1 Flyout control with MVVM) instead.
This seems to circumvent the problem I was having with the above code. Now my shared Flyout code looks like this:
<Flyout x:Name="InfoFlyout"
helpers:FlyoutHelpers.Parent="{Binding ElementName=MyListView}"
helpers:FlyoutHelpers.IsOpen="{Binding IsOpen, Mode=TwoWay}">
<Grid>
<Button Foreground="White" Margin="5">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Help"/>
</StackPanel>
</Button>
</Grid>
</Flyout>
My ListView control (i.e. x:Name="MyListView") is binded to the page's ViewModel i.e. MainPageViewModel. The IsOpen property is defined in the MainViewModel.
Now in my ListView DataTemplate, I want my Flyout to open when I press and hold the ListViewItem or when pressing a button within the ListViewItem:
<DataTemplate>
<Grid FlyoutBase.AttachedFlyout="{StaticResource InfoFlyout}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Source={Binding MyImage} />
<Grid Grid.Column="1" Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button Width="30" Height="30"
Flyout="{StaticResource InfoFlyout}"
content="i">
</Button>
</Grid>
<interactivity:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="Holding">
<actions:OpenFlyoutAction />
</core:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Grid>
</DataTemplate>
As you can see, I've got the Flyout "attached" to the Grid via:
FlyoutBase.AttachedFlyout="{StaticResource InfoFlyout}"
and I've got the same Flyout attached to the button within the ListViewItem itself via:
Flyout="{StaticResource InfoFlyout}"
I've put breakpoints on both my setter and getter for the IsOpen property and when page gets loaded, it does go into the getter but whenever I open or close my Flyout either via Holding or by pressing the 'i' button, it doesn't trigger the method below and therefore it doesn't change the IsOpen property.
private static void OnIsOpenPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e) as defined in the FlyoutHelper class.
The reason I've set my ElementName to MyListView is that I want all my ListViewItem to be binded to the one property i.e. IsOpen as I need to detect whenever a flyout menu is opened irrelevant of which ListViewItem it belongs to.
How can I achieve or resolve this?
UPDATE - 1
The problem of accessing the shared menu has been resolved by using the following:
<Flyout x:Name="InfoFlyout"
helpers:FlyoutHelpers.Parent="{Binding ElementName=MyListView}"
helpers:FlyoutHelpers.IsOpen="{Binding IsOpen, Mode=TwoWay}">
and setting the button to
<Button Width="30" Height="30"
Command="{Binding InformationCommand}"
CommandParameter="{Binding}"
Flyout="{StaticResource InfoFlyout}">
Which is fine and as #ElvisXia mentioned, you can comment out the code in the OnIsOpenPropertyChanged as the positioning is already determined by the button located inside my ListViewItem.
There is however one outstanding problem. A small one btw, but nice if it can be solved. The shared flyout which is attached to the grid itself in the DataTemplate i.e.
<DataTemplate>
<Grid FlyoutBase.AttachedFlyout="{StaticResource InfoFlyout}">
It is being positioning based on the ListViewItem which technically is correct as I'm calling a different piece of code for that one i.e.
<interactivity:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="Holding">
<actions:OpenFlyoutAction />
</core:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
And the OpenFlyoutAction is defined as follows:
public class OpenFlyoutAction : DependencyObject, IAction
{
public object Execute(object sender, object parameter)
{
FrameworkElement senderElement = sender as FrameworkElement;
FlyoutBase flyoutBase = FlyoutBase.GetAttachedFlyout(senderElement);
flyoutBase.ShowAt(senderElement);
return null;
}
}
Can I somehow stop using the OpenFlyoutAction and use the same code as provided in the article to open my Flyout wherever the user is holding his/her finger on the relevant ListViewItem rather than on top or below the actual ListViewItem?
I understand it's a little bit side track from the original issue which was to share a Flyout by to controls but may as well finish it as it is somehow relevant to the issue.
Thanks.
Change the type of Parent from Button to ListView. To open flyout in particular X,Y position is not possible in WP. You can choose PopUp control instead. Here is a link which i got open the pop up in tapped position. You can use VisualTreeHelper to get PopUp control of tapped ListViewItem
By Using Windows 8.1 Flyout control with MVVM , the author use parent to control where the flyout shows up.
So the author have codes like below(FlyoutHelpers.cs):
private static void OnIsOpenPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var flyout = d as Flyout;
var parent = (ListView)d.GetValue(ParentProperty);
if (flyout != null && parent != null)
{
var newValue = (bool)e.NewValue;
if (newValue)
flyout.ShowAt(parent);
else
flyout.Hide();
}
}
He use flyout.ShowAt(parent) to let flyout show at parent element. But in your codes you have binded the flyout to the button using:
<Button Width="30" Height="30"
Flyout="{StaticResource InfoFlyout}" content="i">
</Button>
So it is not necessary to let it show at it's parent any more. To fix the problem, you can comment out the statements like below:
private static void OnIsOpenPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
//var flyout = d as Flyout;
//var parent = (ListView)d.GetValue(ParentProperty);
//if (flyout != null && parent != null)
//{
// var newValue = (bool)e.NewValue;
// if (newValue)
// flyout.ShowAt(parent);
// else
// flyout.Hide();
//}
}
Then you will see the flyout shows at the right place.
I am developing an Windows Phone 8.1 App with Prism for Windows Store Apps and Unity as IoC container. I got the basics working so far and the convention based resolving of View and ViewModel works so far.
For navigation I want to use a Drawer Layout (with a flyout menu) and display the selected models inside the MainPage (rather than navigating to pages).
So I have a MainViewModel (which gets correctly instantiated and assigned with MainPage, using a custom name convention). And I have a DrawerViewModel. The drawer is inside the MainPage, where also the other ViewModels shall be displayed.
My MainPage.xaml:
<Page
x:Class="StackExchangeReader.Views.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:StackExchangeReader"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:m="using:StackExchangeReader.Models"
xmlns:drawerLayout="using:DrawerLayout"
xmlns:prism="using:Microsoft.Practices.Prism.Mvvm"
xmlns:designViewModels="using:StackExchangeReader.DesignViewModels"
mc:Ignorable="d"
prism:ViewModelLocator.AutoWireViewModel="True"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
d:DataContext="{d:DesignInstance designViewModels:DrawerDesignViewModel, IsDesignTimeCreatable=True}">
<Grid x:Name="RootLayout">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Title Bar -->
<Grid x:Name="TitleBar" Background="#00ADEF" Grid.Row ="0" Height="60">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Margin="5" x:Name="DrawerIcon" Grid.Column="0" Source="/Assets/drawer_icon.png" HorizontalAlignment="Left" Tapped="DrawerIcon_Tapped" />
<TextBlock Grid.Column="1" Text="{Binding Title}" Foreground="White" VerticalAlignment="Center" FontSize="18"/>
</Grid>
<!-- Drawer Layout -->
<drawerLayout:DrawerLayout Grid.Row="1" x:Name="DrawerLayout">
<Grid x:Name="MainFragment" Background="White">
<!-- Main Content goes here -->
</Grid>
<Grid x:Name="ListFragment" Background="#F4F4F4">
<ListView DataContext="{Binding Sites}">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Name}" Margin="10" HorizontalAlignment="Left" VerticalAlignment="Center" FontSize="18" Foreground="Black" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</drawerLayout:DrawerLayout>
</Grid>
</Page>
The two things here to be notice are:
MainFragment Grid, where the other ViewModels shall be displayed (i.e. after clicking an option in the Drawer Layout menu)
ListFragment Grid, with the options the user can select
Now the actual problem/question:
Since Prism resolves ViewModels and Views by convention, of course...
... it won't resolve the DrawerViewModel.
I can't instantiate it in XAML, cause it doesn't have a parameterless constructor and it has dependencies injected, so it's required that it's resolved via Unity.
Adding a property in MainViewModel could be possible, but then I'd need to inject it. While it would solve this one problem, it doesn't solve the problem that I also need to instantiate the Content (inside MainFragment) too. Also it's a bad idea to make ViewModels independent of each other. After all, MVVM should support loose coupling.
I also tried to define ViewModel dependency on the MainPage class, but Prism/Unity do not seem to be call this constructor (which Unity should do, if they were to be resolved via IoC/DI, because Unity chooses the constructor with the highest number of dependencies when it resolves the class)
I could also inject the IoC container into the MainViewModel, but this makes my ViewModel(s) depend on IoC. And passing IoC into ViewModel is a bad practice anyways.
I also considered using an abstract factory. This would avoid dependency on IoC container, but it feels like it only shifts the problem.
First things first:
What would be the correct and elegant way to assign an instance of DrawerViewModel as ListFragment's DataContext?
Could designing the ListFragment as an UserControl help allow me that the DrawerViewModel is correctly resolved and assigned to it? Anyone (preferably with Enterprise Business Application experience) having the same issues and a solution for this problem?
Just for the record, the two ViewModel classes.
MainViewModel.cs
public class MainViewModel : ViewModel
{
private INavigationService navigation;
private IEventAggregator events;
public MainViewModel(INavigationService navigation, IEventAggregator events)
{
this.navigation = navigation;
this.events = events;
}
public string Title { get; set; } = "Example";
private ViewModel selectedViewModel;
public ViewModel SelectedViewModel
{
get { return selectedViewModel; }
set { SetProperty(ref selectedViewModel, value); }
}
}
DrawerViewModel.cs
public class DrawerViewModel : ViewModel
{
private ISiteService siteService;
private IEventAggregator events;
public DrawerViewModel(ISiteService siteService, IEventAggregator events)
{
this.siteService = siteService;
this.events = events;
sites = new ObservableCollection<Site>();
}
private ObservableCollection<Site> sites;
public ObservableCollection<Site> Sites
{
get { return sites; }
}
}
See the following XAML:
<UserControl xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk" x:Class="SilverlightApplication1.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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400" MinHeight="150">
<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<sdk:DataGrid x:Name="grid" VerticalScrollBarVisibility="Visible" />
<Button x:Name="button" Grid.Row="1" Content="hello" VerticalAlignment="Top" HorizontalAlignment="Center" Click="button_Click" />
</Grid>
</UserControl>
Corresponding code:
public partial class MainPage : UserControl
{
public class dataclass
{
public string data { get; set; }
}
ObservableCollection<dataclass> list;
public MainPage()
{
InitializeComponent();
grid.ItemsSource = list = new ObservableCollection<dataclass>();
}
private void button_Click(object sender, RoutedEventArgs e)
{
for (var i = 0; i < 5; i++)
list.Add(new dataclass
{
data = "hello" + i
});
}
}
How it works now: The grid takes up the entire screen height minus the height of the button. When too many new items are added, you scan start scrolling. The position of the button never changes, it's always at the bottom of the screen.
What I would like: The grid should take up as little space as possible, so when it's empty, only the header should be visible, and the button immediately below it. When too many items are added, and the button is already at the bottom of the screen, it shouldn't grow any more, but start scrolling instead.
If I swap the two RowDefinition's, then the grid is small at first, but grows indefinitely, pushes the button off the screen and never starts scrolling. How can I do this nicely?
In order to achieve this, nest another Grid in LayoutRoot and then use that nested grid as your main one. Then set both rows to Auto.
<Grid x:Name="LayoutRoot" Background="White">
<Grid x:Name="innerGrid">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid x:Name="itemInTheFirstRow" Grid.Row="0" />
<Button x:Name="itemInTheSecondRow" Grid.Row="1" />
</Grid>
</Grid>
Finally, you need to track the size of the grid and change the sizing rules accordingly. This "pinning" code looks something like.
RowDefinition row = this.innerGrid.RowDefinitions[0];
if (row.Height.GridUnitType == GridUnitType.Auto)
{
if (this.innerGrid.ActualHeight > this.ActualHeight)
{
row.Height = new GridLength(1, GridUnitType.Star);
}
}
else
{
if (this.itemInTheFirstRow.DesiredSize.Height < row.ActualHeight)
{
row.Height = GridLength.Auto;
}
}
In my implementation, I wrap this code in an UpdateRowPinning method that actually uses the dispatcher to call this code. I then call UpdateRowPinning on resize events for the main grid and the inner grid as well as on adding and removing items from the grid and expand/collapse operations of grid groups. This ensures that the second row behaves properly by sitting at the base of the first row until the screen is full and then floating over it after that.
My answer here also covers this issue. I searched for a XAML only solution but it just doesn't seem possible (unless you write some XAML extensions, then you might be able to pull it off with XAML but that's kind of cheating).