I'm trying to implement the equivalent of a WPF lookless control using Xamarin Forms. That is:
a class that derives from ContentView
a Style which sets (implicitly) its ControlTemplate
a Template which contains its UI
I'm using this approach so that the control can expose some bindable properties which can be set by its user, properties to which then elements from its ControlTemplate TemplateBind. It's working great and the UI loads as expected.
However, I have now the problem of handling events from its UI children. For example, I have some grids in its ControlTemplate and I'd like to handle the Tapped event on them.
I tried the following:
Adding, in XAML, a <Grid.GestureRecognizers>, where TappedCommand is a command exposed by the control. This throws InvalidOperationException at app load with "invalid state" or something.
Same as above, but instead of using a Command, using an EventTrigger and a custom ExecuteCommandAction which inherits TriggerAction<View>. My ExecuteCommandAction's Invoke is never called.
Same as above, but using a normal Tapped event handler . This works, but the handler is in the App.xaml.cs, so not really helpful, I need it to be in the control itself.
What I can additionally think of trying is declaring x:Name on the Grid in the template and referencing it from the control, then programatically adding a TappedGestureRecognizer to it, but I don't know where to do this. In WPF there was an OnApplyTemplate() method where it was "safe" to look in the template for children by name. Is there something similar in Xamarin Forms?
Any other suggestions are appreciated, thanks a lot!
Later edit, here's the code:
public class NextPrevDateSelector : ContentView
{
public NextPrevDateSelector ()
{
this.ShowPreviousDayCommand = new Command(() =>
{
/// This is where I'd like to handle the taps on the
/// element (Grid or Button or whatever) inside the template
/// which should change the date to the previous day. This
/// need not be a Command, if I could register an event handler
/// to the Tapped event that'd also be fine
});
}
// These events would be subscribed to by users of the control.
// They'd probably be commands, not events, but I put events here to
// keep the code small.
public event EventHandler PreviousDaySelected;
public event EventHandler NextDaySelected;
// This property can be used by users of the control to customize how the text inside the control looks like, the elements in the control's template will TemplateBind to this
public static readonly BindableProperty TextStyleProperty = BindableProperty.Create(
propertyName: "TextStyle",
returnType: typeof(Style),
declaringType: typeof(NextPrevDateSelector),
defaultValue: null);
public Style TextStyle
{
get { return (Style)GetValue(TextStyleProperty ); }
set { SetValue(TextStyleProperty , value); }
}
public ICommand ShowPreviousDayCommand { get; private set; }
}
And here's the XAML in the App.xaml ResourceDictionary
<Style TargetType="dateSelector:NextPrevDateSelector">
<Setter Property="ControlTemplate" Value="{StaticResource DateSelectorTemplate}" />
</Style>
And the control template
<ControlTemplate x:Key="DateSelectorTemplate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid x:Name="prevDayButton" Grid.Column="0" Padding="20,0,20,0" HorizontalOptions="Start">
<Label Grid.Column="0" Text="<" Style="{TemplateBinding TextStyle}" />
</Grid>
<Grid Grid.Column="1" HorizontalOptions="Center">
<Label Text="Today etc" Style="{TemplateBinding TextStyle}" />
</Grid>
<Grid x:Name="nextDayButton" Grid.Column="2" Padding="20,0,20,0" HorizontalOptions="End">
<Label Text=">" Style="{TemplateBinding TextStyle}" />
</Grid>
</Grid>
</ControlTemplate>
I'd need to know when prevDayButton, nextDayButton and the center button were tapped. As mentioned I tried this first:
<Grid x:Name="prevDayButton" Grid.Column="0" Padding="20,0,20,0" HorizontalOptions="Start">
<Label Grid.Column="0" Text="<" Style="{TemplateBinding TextStyle}" />
<Grid.GestureRecognizers>
<TapGestureRecognizer Command={TemplateBinding ShowPreviousDayCommand}/>
</Grid.GestureRecognizers>
</Grid>
This throws at app initialization. Next I tried this:
<Grid x:Name="prevDayButton" Grid.Column="0" Padding="20,0,20,0" HorizontalOptions="Start">
<Label Grid.Column="0" Text="<" Style="{TemplateBinding TextStyle}" />
<Grid.GestureRecognizers>
<TapGestureRecognizer>
<EventTrigger Event="Tapped">
<behaviors:ExecuteCommandAction/>
</EventTrigger>
</TapGestureRecognizer>
</Grid.GestureRecognizers>
</Grid>
...where ExecuteCommandAction is a TriggerAction<View>, but its Invoke method is never called. So I'm left with adding a normal event handler to the "buttons", but I can't add it in XAML as the handler needs to be in my control and I can't add it in my control because I can't find a way to get a reference to the buttons so that I can add a gesture recognizer to them.
I hope the code helps make the question clearer.
Related
I have a ListView in my ContentPage. Inside ListView.ItemTemplate, I have user control. In user control, there are image buttons. On Click of ImageButton, I want to move another page but it's not working. The code is run without error but nothing happened. The same code of PushAsync is working from all other content pages but not in user control. Please help me in this regard to what should I do.
ListView Code inside ContentPage:
<ListView x:Name="lvPosts" HasUnevenRows="True" SeparatorVisibility="None"
IsVisible="False">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Grid Padding="10">
<controls:CardView/>
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
UserControl CardView:
<Frame xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:local="clr-namespace:MyApp.General"
VerticalOptions="Start"
x:Class="MyApp.Controls.CardView">
<Frame.Content>
<Grid Padding="10" >
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="100" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label
Grid.Column="0"
Grid.ColumnSpan="4"
Grid.Row="0"
Text="{Binding Title}"
HorizontalOptions="FillAndExpand"
TextColor="#479774"
FontSize="Medium"
Style="{StaticResource DefaultFontStyle}"
Opacity="0.8">
</Label>
<StackLayout Orientation="Vertical" Grid.Row="1" Grid.Column="0">
<ImageButton
x:Name="imgPdf"
Source="pdf.png"
Aspect="AspectFill"
Clicked="imgPdf_Clicked"/>
</StackLayout>
<StackLayout Orientation="Vertical" Grid.Row="1" Grid.Column="1">
<ImageButton
x:Name="imgContent"
Source="document2.png"
Aspect="AspectFill"
Clicked="imgContent_Clicked"/>
</StackLayout>
<StackLayout Orientation="Vertical" Grid.Row="1" Grid.Column="2">
<ImageButton
x:Name="imgSummary"
Source="document.png"
Aspect="AspectFill"
Clicked="imgSummary_Clicked"/>
</StackLayout>
<StackLayout Orientation="Vertical" Grid.Row="1" Grid.Column="3">
<ImageButton
x:Name="imgVideo"
Source="video1.png"
Aspect="AspectFill"
Clicked="imgVideo_Clicked"/>
</StackLayout>
</Grid>
</Frame.Content>
</Frame>
And Code behind on ImageButton:
private async void imgVideo_Clicked(object sender, EventArgs e)
{
Post post = this.BindingContext as Post;
await Navigation.PushAsync(new WebViewPage(post));
}
The above answer isn't quite correct, and makes some assumptions. Navigation is available and can be used by any Xamarin Forms component that inherits NavigableElement and has a NavigationPage as an ancestor in its parental hierarchy. It's not limited to ContentPage by any means. Internal to Xamarin Forms, NavigableElements access their ancestral NavigationPage's Navigation stack via a series of internal NavigationProxy classes that get set and bound to their parent's NavigationProxy when each element's parent is set. Externally, we see these proxies as their interface type of INavigation, typically as the Navigation property of whatever element we're using.
ViewCell does not inherit NavigableElement (why, I'm not entirely sure, since it's parented to its container like any other component), and thus doesn't have a Navigation property. In your case, your ViewCell contains another View, which does inherit NavigableElement, but because its parent (ViewCell) isn't a NavigableElement, the chain of NavigationProxies is broken. Thus, you can try to make calls to Navigation on your CardView, but it's not hooked up to anything, and thus won't do anything.
The solution of directly accessing the Application's Navigation property (I assume the above poster meant Application.Current.MainPage.Navigation, as Application doesn't have a Navigation property itself) only works if your Application's MainPage is the only NavigationPage in your app. However, this isn't guaranteed to be the case; for example, a TabbedPage component could contain its own NavigationPages for each of its tabs, and accessing Application.Current.MainPage.Navigation will only get you the NavigationProxy for navigating the MainPage. ViewCells in a tabbed NavigationPage would be unable to navigate within their tab via this solution.
What you can do instead is something like this:
public MyViewCell : ViewCell
{
private INavigation Navigation { get; set; }
protected override void OnParentSet()
{
base.OnParentSet();
Navigation = (Parent as NavigableElement)?.Navigation;
}
}
Your ViewCell now has access to its parent's navigation stack, which you can do with as you please. You could pass it into your cell's child View, if you wanted, or just do your navigation here. Why ViewCell doesn't already do this under the hood, I don't fully understand.
Note: Depending on the CacheStrategy you set on your ListView (or whatever View component is using your ViewCell as a template), it's possible your custom ViewCell may be handed a null Parent from time to time, which would yield a null Navigation property. Usually this shouldn't be a problem, as a null Parent generally indicates the cell isn't actively being displayed, but it's worth keeping in mind in case your custom ViewCell could potentially try to access Navigation without user input via some automated business logic.
We could only invoke the method Navigation.PushAsync in a ContentPage . In your case , CardView is a subclass of a Frame , so it will never work .
The best solution is to invoke the method in Code Behind (ViewModel or ContentPage) . Since you had used Custom View . It would be better to handle the logic by using Data-binding .
If you do want to invoke the method in CardView, you could define a property in App and pass the current Navigation from ContentPage .
in App.xaml.cs
public INavigation navigation { get; set; }
in ContentPage
Note : You need to invoke the following lines in each ContentPage .
public xxxContentPage()
{
InitializeComponent();
var app = App.Current as App;
app.navigation = this.Navigation;
}
in CardView
var app = App.Current as App;
var navigation = app.navigation;
navigation.PushAsync(xxx);
I have an application that displays a datagrid. However the data has gotten big and I want to incorporate filters to some of the rows. I've gotten as far as creating a DataTemplate for my headers:
<DataGrid>
<DataGrid.Resources>
...
<DataTemplate x:Key="HeaderTemplate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ContentControl Content="{Binding}" VerticalAlignment="Center"/>
<ToggleButton Name="FilterButton" Grid.Column="1" Content="▼" Margin="2, 1, 1, 1" Padding="1, 0"/>
<Popup IsOpen="{Binding ElementName=FilterButton, Path=IsChecked}" PlacementTarget="{Binding ElementName=FilterButton}" StaysOpen="False">
<Border Background="White" Padding="3">
<TextBox Text={Binding PetNameFilterSearchBox, Mode=TwoWay} Width="300"/> <!--The Text Box I want to bind-->
</Border>
</Popup>
</Grid>
</DataTemplate>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn Width="6*" Header="Pet Name" Binding="{Binding PetName}" ElementStyle="{DynamicResource DataGridTextColumnWrap}" HeaderTemplate="{StaticResource HeaderTemplate}"/>
</DataGrid.Columns>
</DataGrid>
So far what it does is show a button next to the header text and when you click on it a small popup window appears containing a text box. The desired effect is that the user can type in the text box and data will be filtered according to what was typed.
In my view model I already have my filter text box property that I want to use for binding:
public string PetNameFilterSearchBox
{
get
{
return _petNameFilterSearchBox;
}
set
{
_petNameFilterSearchBox = value;
RaisePropertyChanged(nameof(PetNameFilterSearchBox));
FilterData(); //As you're writing
}
}
private string _petNameFilterSearchBox = string.Empty;
public CollectionView PetDataFilterView { get; set; }
public bool OnFilterTriggered(object item)
{
if (item is AvailablePetInfo petInfo)
{
var pet_name = PetNameFilterSearchBox;
if (pet_name != string.Empty)
return (petInfo.DisplayName.Contains(pet_name));
}
return true;
}
public void FilterData()
{
CollectionViewSource.GetDefaultView(AvailablePetInfo).Refresh();
}
//Constructor
public PetInfoViewModel()
{
AvailablePetInfo = GetPetInfo();//gets the list
ContactFilterView = (CollectionView)CollectionViewSource.GetDefaultView(AvailablePetInfo);
ContactFilterView.Filter = OnFilterTriggered;
}
When I run my code I see the little button next to the header, I click on it and I see the textbox. But when I start typing I dont see my datagrid updating. I set some breakpoints in my PetNameFilterSearchBox and I find that when I start typing it's not getting hit. This tells me that there's something wrong with the binding. Can someone tell me what I'm doing wrong?
Your problem is one of DataContext.
I'll be assuming PetNameFilterSearchBox is a property of the Window hosting the DataGrid and that the appropriate DataContext is set at the Window level.
Normally, DataContext is inherited by child elements, so setting the DataContext for the Window would set it for all its children. But things change once you start using DataTemplates.
In a DataTemplate, the root DataContext is always the data object that's being displayed. In your case, that's the string "Pet Name". This is why you can put <ContentControl Content="{Binding}"/> inside the DataTemplate and have it display "Pet Name".
The downside is you can't put <TextBox Text="{Binding PetNameFilterSearchBox}"/> and expect it to bind to the Window, because that DataContext is being overridden by the DataTemplate.
Normally, you can get around the DataTemplate DataContext problem by using RelativeSource, which you can use walk up the visual tree and find another source to bind to. But this doesn't work inside a Popup because a Popup is not actually part of the Window's visual tree.
What will work is ElementName:
<TextBox Text="{Binding PetNameFilterSearchBox, Mode=TwoWay, ElementName=W}" Width="300"/>
In the above example, I set on my Window Name="W".
I have some WPF control called Foo. Grid structure with DevExpress LoadingDecorator looks like that:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="60" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<dx:LoadingDecorator Name="Decorator" IsSplashScreenShown="{Binding Path=ShowLoader}" SplashScreenLocation="CenterWindow">
<dx:LoadingDecorator.SplashScreenTemplate>
<DataTemplate>
<dx:WaitIndicator DeferedVisibility="True">
<dx:WaitIndicator.ContentTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<TextBlock Text="Operation:" FontSize="15"/>
<TextBlock Text="{Binding Path=CurrLoadStat}"/>
</StackPanel>
</DataTemplate>
</dx:WaitIndicator.ContentTemplate>
</dx:WaitIndicator>
</DataTemplate>
</dx:LoadingDecorator.SplashScreenTemplate>
</dx:LoadingDecorator>
</StackPanel>
...
</Grid>
Base ViewModel class implements INotifyPropertyChanged interface and ViewModel class (FooViewModel) used as DataContext for control with Grid described above inherits from it. I have implemented property to change text property in 2nd TextBlock element:
private string currLoadStat = "Preparing data...";
public string CurrtLoadStat
{
get
{
return currLoadStat;
}
set
{
currLoadStat = value;
OnPropertyChanged("CurrLoadStat");
}
}
My problem is that binding instruction doesn't work and I see only text defined in first TextBlock.
Can You provide me some solution to resolve this problem?
Your property has a "t" in the middle of the name but your binding path (XAML) and magic string you pass to OnPropertyChanged do not. The string you pass when you raise the PropertyChanged event method must exactly match your view model's property name.
If you are using C# 5 or 6 then switch to using one of the approaches outlined here and you would eliminate the need for passing magic strings.
I have a XAML main window that contains a header, a central area and a footer (in a grid). The central area contains a ContentControl which is set throw a binding (using MVVMLight). The header/footer is always the same so no problems there.
The part that goes into the ContentControl is always quite similar, they are WPF usercontrols and have a left part that contains info and a right part with at least an OK and BACK button.
These are viewmodels and their views:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<TextBlock Text="this changes and contains other controls too" />
</Grid>
<Grid Grid.Column="1">
<!-- more buttons and statuses-->
<Button Content="Back" Margin="5" Height="30" />
<Button Content="Ok" Margin="5" Height="30" />
</Grid>
</Grid>
Is there a way i could create a base class/custom control for those views? So that I could write something like this in my xaml:
<basewindow>
<leftpart>
custom XAML for this view
</leftpart>
<rightpart>
custom XAML for this view
</rightpart>
</basewindow>
I could then remove duplicate code that is now in each of those views to the base class while still keeping the ability to write my xaml in the editor. Or is this not feasible?
To clarify are you trying to inherit the visual element that exist in XAML, like you can do in WinForms? If so you cannot do this in WPF. There is no Visual inheritence in WPF.
Now if you aren't trying to inherit visual element it is easy. First create your UserControlBase class and add you event handler. Keep in mind this base class can not have any XAML associated with it. Code only
public class MyUserControlBase : UserControl
{
public MyUserControlBase()
{
}
protected virtual void Button_Click(object sender, RoutedEventArgs e)
{
}
}
Now create another UserControl that does have a xaml counter part. Now you will need to change the root elemtn in the XAML to your base class like this:
<local:MyUserControlBase x:Class="WpfApplication7.MyUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication7">
<Grid>
<Button Click="Button_Click">My Button</Button>
</Grid>
</local:MyUserControlBase>
And don't forget the code behind:
public partial class MyUserControl : MyUserControlBase
{
public MyUserControl()
{
InitializeComponent();
}
}
Notice the button in the derived user control is calling the Button_Click event handler we defined in the base class. That is all you need to do.
I am using MVVM for my application and have a form that allows the user to enter basic personnel information. The form includes a UserControl which is, basically, an ItemsControl that includes textBoxes that can be dynamically created. This is a simplified version:
<ItemsControl x:Name="items" ItemsSource="{Binding MyItemsCollection}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid x:Name="row">
<TextBox x:Name="textBox" Text="{Binding ContactInfo, ValidatesOnExceptions=True}" extensions:FocusExtension.IsFocused="{Binding IsFocused}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button x:Name="NewItemButton" Command="{Binding AddItemToMyCollectionCommand}" />
I want the TextBox that has just been created to receive focus, therefore I added an attached property. This is part of it:
public static readonly DependencyProperty IsFocusedProperty =
DependencyProperty.RegisterAttached("IsFocused", typeof(bool), typeof(FocusExtension), new UIPropertyMetadata(false, OnIsFocusedPropertyChanged));
private static void OnIsFocusedPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var uie = (UIElement)d;
if ((bool)e.NewValue)
{
uie.Focus();
}
}
In the form that contains the UserControl there are several other text boxes before and after. The UserControl has its own ViewModel, which I set as the DataContext of the control through a property in the container's ViewModel. Basically, a simplified version of the container looks like this:
<StackPanel Orientation="Horizontal" />
<TextBox x:Name="firstName" />
<TextBox x:Name="lastName" />
<local:DynamicFormUserControl
x:Name="phones"
DataContext="{Binding PhonesViewModel}" />
<local:DynamicFormUserControl
x:Name="emails"
DataContext="{Binding EmailsViewModel}" />
<TextBox x:Name="address" />
</StackPanel>
My problem is that I want the firstName TextBox to get the focus when the form is loaded for the first time, but the form keeps on placing the focus on the first TextBox of the phones UserControl. I tried to override it by using firstName.Focus() on the Loaded event of the form, but this didn't work, and no matter what I tried the focus is still on the phones userControl instead of the first element in the form that contains it.
Does anybody have any idea how to solve this?
Thanks.
Here you go
add FocusManager.FocusedElement="{Binding ElementName=firstName}" to your stack panel
<StackPanel Orientation="Horizontal"
FocusManager.FocusedElement="{Binding ElementName=firstName}"/>
<TextBox x:Name="firstName" />
<TextBox x:Name="lastName" />
<local:DynamicFormUserControl
x:Name="phones"
DataContext="{Binding PhonesViewModel}" />
<local:DynamicFormUserControl
x:Name="emails"
DataContext="{Binding EmailsViewModel}" />
<TextBox x:Name="address" />
</StackPanel>
also notice that you may need to prevent items control in the user control from focusing itself
<ItemsControl x:Name="items" Focusable="False" >
<ItemsControl.ItemTemplate>
I guess I managed to find a solution. The problem was that the form I created was itself a user control inside a window, and never got focus. I didn't think that would be relevant so I didn't mention it in my previous post- sorry. I found in this solution for forcing focus to a user control.
Basically, when I have a UserControl inside a window it doesn't get focus even if I try to set the focus with either Focus() or FocusedElement. So to overcome this problem I found on a different post a workaround. Basically I added it to the code-behind of the UserControl that contains the firstName TextBox. If we call the UserControl, say, PersonalInfoUserControl, the constructor of the control would look like this:
public PersonalInfoUserControl()
{
InitializeComponent();
this.IsVisibleChanged += new DependencyPropertyChangedEventHandler(UserControl_IsVisibleChanged);
}
I added an event handler to the IsVisibleChanged event of the control. The method would look like this:
void UserControl_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if ((bool)e.NewValue == true)
{
Dispatcher.BeginInvoke(
DispatcherPriority.ContextIdle,
new Action(delegate()
{
firstName.Focus();
}));
}
}