I'm following this example for an auto-scrolling behavior on a ListBox on WP7 Mango RC targeting 7.1.
In my Xaml:
<ListBox x:Name="StatusMessages"
Height="100"
ItemsSource="{Binding StatusMessages, Mode=TwoWay}"
DisplayMemberPath="Message"
Grid.Row="3">
<i:Interaction.Behaviors>
<behaviors:ListBoxItemAutoScrollBehavior FoundItem="{Binding FoundItem}" />
</i:Interaction.Behaviors>
</ListBox>
The behavior:
public class ListBoxItemAutoScrollBehavior : Behavior<ListBox>
{
public object FoundItem
{
get { return GetValue(FoundItemProperty); }
set { SetValue(FoundItemProperty, value); }
}
public static readonly DependencyProperty FoundItemProperty = DependencyProperty.Register("FoundItem", typeof (object), typeof (ListBoxItemAutoScrollBehavior), new PropertyMetadata(FoundItemChanged));
private static void FoundItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((ListBoxItemAutoScrollBehavior) d).AssociatedObject.ScrollIntoView(e.NewValue);
}
}
I have a breakpoint set at the FoundItemChanged method and would expect to see it hit when I set FoundItem in my ViewModel and fire NotifyProperyChanged. Only, it doesn't work, any ideas why or what I might be doing wrong?
Thanks.
update: breakpoints are hit for OnAttached and OnDetaching in the behavior.
update 2: This works in a regular Silveright 4 application.
update 3: Using version 3.8.5.0 of System.Windows.Interactivity fixed it.
Are you targeting 7.1 / Mango? Binding to DependencyObjects, as opposed to FrameworkElements, is a Silverlight 4 feature and thus not available in 7.0 (which uses SL 3).
There's a workaround that Prism and MVVM Light use to bind to DO's in SL 3. Check out their source for details.
Edit: Your problem is your PropertyMetadata constructor arguments. By not specifying 2 arguments (or, specifically, passing a method rather than a PropertyChangedCallback instance), the compiler might be resolving the default value constructor overload.
In short, change it to:
new PropertyMetadata(null, FoundItemChanged)
Or:
new PropertyMetadata(new PropertyChangedCallback(FoundItemChanged))
Using version 3.8.5.0 of System.Windows.Interactivity fixed this problem.
This post gave me the tip: http://caliburnmicro.codeplex.com/discussions/271092
Related
.Net Maui page with CarouselView and list of cards created via data binding to the collection of items in ViewModel (VM). I'm looking for ways to animate control inside CarouselView by some property in VM set to a particular value. Animation should be done in c# code (code-behind, trigger action, behavior etc.), not by xaml. Not sure how to properly implement that. This is what I considered as possible solutions:
Declare event in VM and subscribe for it in code-behind. Works very well for non-template controls, but with CarouselView which consists of collection Card controls described inside DataTemplate I need to find that particular active control only, let's say Label that I want to animate. Not sure how to find it, there are one instance of it per each item in VM collection, but even if I do it does not look to be a good MVVM oriented design.
My big hope was on TriggerAction<Label> (given I want to animate Label), but then problem is TriggerAction seems to only work inside EventTrigger which only catches xaml control events, not VM events. What can catch VM events and property changes is DataTrigger, but it does not allow to have TriggerAction<T> declared inside on the other hand. Not sure why there is such limitation in .Net Maui, I wish I had some mix of both.
Behaviors, - as with triggers not sure how to run them by any property change or event declared in VM
// ViewModel (mvvm community toolkit is used):
public partial class CollectionOfItems : ObservableObject
{
public Item[] Items {get; set;}
}
public partial class Item : ObservableObject
{
[ObservableProperty]
public string _name;
// Setting this to false should trigger Label animation
[ObservableProperty]
public bool _isInvalid;
}
...
<CarouselView ItemsSource="{Binding Items}">
<CarouselView.ItemTemplate>
<DataTemplate>
<Label Text="{Binding Name}">
<Label.Triggers>
<DataTrigger TargetType="Label" Binding="{Binding IsInvalid}" Value="True">
<!-- I wish I was able to trigger InvalidDataTriggerAction from here, but it's not compatible with DataTrigger -->
</DataTrigger>
</Label.Triggers>
</Label>
</DataTemplate>
</CarouselView.ItemTemplate>
</CarouselView>
...
// Animation should be implemented in c#, not XAML
// Should be triggered by IsInvalid=false, but I do not know how:
public class InvalidDataTriggerAction : TriggerAction<Label>
{
protected override void Invoke(Label label)
{
label.TranslateTo(-30, 0, 100);
label.TranslateTo(60, 0, 100);
label.TranslateTo(-30, 0, 100);
}
}
Alright, I've found a way to do it through bindable properties inside behaviors. Happened to be bit more complicated than I expected, but it works. Unfortunately .NET Maui does not provide a better more intuitive way to do that, I guess it's the area for improvement.
Here's the code:
namespace View.Behaviors;
public class AnimateWrongAnswerBehavior : BaseBehavior<VisualElement>
{
public static readonly BindableProperty ShouldAnimateProperty =
BindableProperty.CreateAttached(
"ShouldAnimate",
typeof(bool),
typeof(AnimateWrongAnswerBehavior),
false,
propertyChanged: OnShouldAnimateChanged);
public static void SetShouldAnimate(BindableObject view, VisualElement value) =>
view.SetValue(ShouldAnimateProperty, value);
static async void OnShouldAnimateChanged(BindableObject bindable, object oldValue, object newValue)
{
if ((bool)newValue)
{
await Animate((VisualElement)bindable);
}
}
/// <summary>
/// Implemenation of Animation logic
/// </summary>
static private async Task Animate(VisualElement elementToAnimate)
{
await elementToAnimate.TranslateTo(-30, 0, 100);
await elementToAnimate.TranslateTo(60, 0, 100);
await elementToAnimate.TranslateTo(-30, 0, 100);
}
}
Then you can bind animation to any visual element like Frame:
<ContentPage
xmlns:bh="clr-namespace:View.Behaviors" ...>
<Frame bh:AnimateWrongAnswerBehavior.ShouldAnimate="{Binding IsInvalid}">
</ContentPage>
Just like you said, the control in the datatemplate is hard to access. So I have done a sample to test the TranslateX property of the Label. And I found that label.TranslateTo(60, 0, 100); had the same effect as the label.TranslateX = 60.
According to this, you can create a variable in the Item and binding it to the label in the DataTemplate. And change the item's value in the page.cs. And you can also use the DataTrigger to set the value of label.TranslateX.
Please note that TranslateTo is awaitable and might need the async/await pattern for the element to be animated, especially if more than one TranslateTo is invoked.
TriggerAction with async/await added:
public class TriggerActionTranslateTo : TriggerAction<VisualElement>
{
protected override async void Invoke(VisualElement sender)
{
await sender.TranslateTo(0, 0, 250);
await sender.TranslateTo(30, 0, 250);
await sender.TranslateTo(0, 0, 250);
}
For some reason async/await is not needed with only one TranslateTo and this will animate:
protected override void Invoke(VisualElement sender)
{
sender.TranslateTo(30, 0, 250);
}
XAML:
<Label.Triggers>
<DataTrigger
TargetType="Label"
Binding="{Binding Property}"
Value="True">
<DataTrigger.EnterActions>
<ns:TriggerActionTranslateTo/>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
...
I want to utilise the auto-sizing feature of android textviews in my xamarin forms solution so that as the text length grows, the font sizes shrinks to never overflow the bounds of the label, and doesn't get truncated. I've created a custom Label control to do so and added an android custom renderer. It's not working in Android 7 and below. It is working in Android 8 and above.
According to the docs autosize support was introduced in android 8, but can be supported back to Android 4 with AppCompat.v4. However, my custom rendered label just renders the default font size in Android pre 8. It works fine in 8+ devices, the label text resizes as needed to not overflow the bounds. The accepted answer to this question with a similar issue on native android says it can be to do with not setting a width and height, I've tried setting widthrequest and heightrequest explicitly and it doesn't change anything. Also setting maxlines=1 doesn't change anything. An alternative thread suggests that custom fonts are the culprit. I created a vanilla forms solution using the default device font, and get the same effect.
My code:
internal class AutosizeLabelRenderer : LabelRenderer
{
#region constructor
public AutosizeLabelRenderer(Context context) : base(context)
{
}
#endregion
#region overridable
protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
{
base.OnElementChanged(e);
if (e.NewElement == null || !(e.NewElement is AutoSizeLabel autoLabel) || Control == null) { return; }
TextViewCompat.SetAutoSizeTextTypeUniformWithConfiguration(Control, autoLabel.AutoSizeMinTextSize,
autoLabel.AutoSizeMaxTextSize, autoLabel.AutoSizeStepGranularity, (int)ComplexUnitType.Sp);
}
#endregion
}
public class AutoSizeLabel : Label
{
public int AutoSizeMaxTextSize
{
get => (int)GetValue(AutoSizeMaxTextSizeProperty);
set => SetValue(AutoSizeMaxTextSizeProperty, value);
}
public static readonly BindableProperty AutoSizeMaxTextSizeProperty = BindableProperty.Create(
nameof(AutoSizeMaxTextSize), // the name of the bindable property
typeof(int), // the bindable property type
typeof(AutoSizeLabel)); // the default value for the property
public int AutoSizeMinTextSize
{
get => (int)GetValue(AutoSizeMinTextSizeProperty);
set => SetValue(AutoSizeMinTextSizeProperty, value);
}
public static readonly BindableProperty AutoSizeMinTextSizeProperty = BindableProperty.Create(
nameof(AutoSizeMinTextSize), // the name of the bindable property
typeof(int), // the bindable property type
typeof(AutoSizeLabel)); // the default value for the property
public int AutoSizeStepGranularity
{
get => (int)GetValue(AutoSizeStepGranularityProperty);
set => SetValue(AutoSizeStepGranularityProperty, value);
}
public static readonly BindableProperty AutoSizeStepGranularityProperty = BindableProperty.Create(
nameof(AutoSizeStepGranularity), // the name of the bindable property
typeof(int), // the bindable property type
typeof(AutoSizeLabel)); // the default value for the property
//
}
Not working: Android 7 - text does not shrink
Working as expected: Android 8 and above
Xaml for above images:
<StackLayout HeightRequest="200" WidthRequest="100">
<Label Text="Fixed width and height, sentences get longer, text should shrink" />
<controls:AutoSizeLabel
AutoSizeMaxTextSize="50"
AutoSizeMinTextSize="8"
AutoSizeStepGranularity="1"
BackgroundColor="{StaticResource Shamrock}"
HeightRequest="40"
HorizontalOptions="Start"
MaxLines="1"
Text="A small sentence"
WidthRequest="200" />
<controls:AutoSizeLabel
AutoSizeMaxTextSize="50"
AutoSizeMinTextSize="8"
AutoSizeStepGranularity="1"
BackgroundColor="{StaticResource Shamrock}"
HeightRequest="40"
HorizontalOptions="Start"
MaxLines="1"
Text="A larger sentence that shrinks"
WidthRequest="200" />
<controls:AutoSizeLabel
AutoSizeMaxTextSize="50"
AutoSizeMinTextSize="8"
AutoSizeStepGranularity="1"
BackgroundColor="{StaticResource Shamrock}"
HeightRequest="40"
HorizontalOptions="Start"
MaxLines="1"
Text="An even larger sentence that shrinks more."
WidthRequest="200" />
</StackLayout>
TextView font size changes with the size of the control, which is new in Android 8.0 (API26),therefore, compatibility issues need to be considered when using the previous version.You could change the TextView to AppCompatTextView.
Change your
protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
{
base.OnElementChanged(e);
if (e.NewElement == null || !(e.NewElement is AutoSizeLabel autoLabel) || Control == null) { return; }
AppCompatTextView appCompatTextView = new AppCompatTextView(_context);
appCompatTextView.Text = Element.Text;
appCompatTextView.SetMaxLines(1);
SetNativeControl(appCompatTextView);
TextViewCompat.SetAutoSizeTextTypeUniformWithConfiguration(Control,autoLabel.AutoSizeMinTextSize,autoLabel.AutoSizeMaxTextSize, autoLabel.AutoSizeStepGranularity, (int)ComplexUnitType.Sp);
}
Leo Zhu's answer got me most of the way there. There were a couple of extra steps I needed to take to get it fully working, so I'm posting the code as a separate answer here.
Differences between mine and Leo's answer:
Creating a new native control in scope like Leo suggested meant that it worked for a while but got disposed by the garbage collector and caused an exception when returning to the page after navigating away. To fix this I needed to override a property called ManageNativeControlLifetime to return false, and then manually manage disposing the object by overriding the dispose method and calling Control.RemoveFromParent();. This advice comes from a xamarin staff member in this thread.
Formatting and binding context are not automatically inherited when creating the new native control and need to be set manually. I needed to add those based on my needs using the android specific binding syntax. You may need to add other formatting and binding code based on your needs, I'm just doing font colour, gravity and binding context here.
I set the binding context with
appCompatTextView.SetBindingContext(autoLabel.BindingContext);
Once the binding context was set, I needed to add a new string property to my XF AutoSizeLabel class to pass in through XAML, then use it to set the binding path for the relevant property (In my case the text property). If more than one binding is required, you would need to add multiple new binding path properties for each required property. I set a specific binding like this:
appCompatTextView.SetBinding("Text", new Binding(autoLabel.TextBindingPath));
To facilitate this in my Xamarin Forms Xaml, my Xaml went from <Label Text="{Binding MyViewModelPropertyName}" /> to <controls:AutoSizeLabel TextBindingPath="MyViewModelPropertyName" />
Here's the full code of the renderer:
protected override bool ManageNativeControlLifetime => false;
protected override void Dispose(bool disposing)
{
Control.RemoveFromParent();
base.Dispose(disposing);
}
private AppCompatTextView appCompatTextView;
protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
{
base.OnElementChanged(e);
if (e.NewElement == null || !(e.NewElement is AutoSizeLabel autoLabel) || Control == null) { return; }
//v8 and above supported natively, no need for the extra stuff below.
if (DeviceInfo.Version.Major >= 8)
{
Control?.SetAutoSizeTextTypeUniformWithConfiguration(
autoLabel.AutoSizeMinTextSize,
autoLabel.AutoSizeMaxTextSize, autoLabel.AutoSizeStepGranularity,
(int)ComplexUnitType.Sp);
return;
}
appCompatTextView = new AppCompatTextView(Context);
appCompatTextView.SetTextColor(Element.TextColor.ToAndroid());
appCompatTextView.SetMaxLines(1);
appCompatTextView.Gravity = GravityFlags.Center;
appCompatTextView.SetBindingContext(autoLabel.BindingContext);
appCompatTextView.SetBinding("Text", new Binding(autoLabel.TextBindingPath));
SetNativeControl(appCompatTextView);
TextViewCompat.SetAutoSizeTextTypeUniformWithConfiguration(Control, autoLabel.AutoSizeMinTextSize, autoLabel.AutoSizeMaxTextSize, autoLabel.AutoSizeStepGranularity, (int)ComplexUnitType.Sp);
}
My application viewModel responds to a user clicking a button to see test results:
private void AddDetailRows(List<QuizHistory> quizHistoryList)
{
quizDetails.Children.Clear();
quizDetails.Children.Add(AddData(quizHistoryList));
quizDetails.Children.Add(new LineTemplate());
}
Where quizDetails is the name of an element in the view.
But this doesn't work for me as the view model doesn't know what the view looks like and does not have access to the names of elements.
In a MVVM application, how is this problem solved?
You are completely right, that is not something that ViewModel is responsible of.
So, whatever you want to do with UI is not responsibility of the ViewModel.
If this is really the only option, then you can think of creating boolean properties in your VM and binding them to your views and then changing that boolean from false to true or vice versa on button click command which is binded to your VM.
To simplify it:
MyView.xaml
<StackLayout>
<Button Command="{Binding ShowHideQuizHistoryCommand}" ... />
<StackLayout x:Name="QuizHistory"
IsVisible={Binding ShowQuizHistory }>
//
</StackLayout>
</StackLayout>
MyViewModel.cs
private bool _showQuizHistory ;
public bool ShowQuizHistory
{
get { return _showQuizHistory ; }
set
{
_showQuizHistory = value;
OnPropertyChanged();
}
}
public ICommand ShowHideQuizHistoryCommand => new Command(() =>
{
ShowQuizHistory = !ShowQuizHistory;
});
So, this is just an example based on what you provided in question.
You can also use visual states, converters, triggers and behaviors in order to achieve this, but in my opinion this is the easiest way.
I am using Prism and Autofac with Xamarin.Forms 4.0 with an MVVM architecture. Using the Navigation.NavigateAsync("MyPage") works unless I have a binding to the Date object with my ViewModel.
The page renders properly and I am navigated to it if my DatePicker has no binding.
<DatePicker x:Name="ProcessStartDate" Format="D" MinimumDate="01/01/2000" />
However the following will cause me to never navigate to the page.
<DatePicker x:Name="ProcessStartDate" Format="D" MinimumDate="01/01/2000" Date="{Binding SelectedStartDate, Mode=TwoWay}"
The property in the View Model, MyVM, looks like this.
private DateTime selectedStartDate;
public DateTime SelectedStartDate
{
get
{
return selectedStartDate;
}
set
{
SetProperty(ref selectedStartDate, value);
sample.ProcessStartDate = value;
}
}
Navigation with the following code fails with the Binding in XAML above:
INavigationResult status;
try
{
var parameters = new NavigationParameters();
parameters.Add("CurrentSample", SelectedSample);
status = await NavigationService.NavigateAsync("MyPage", parameters); //MyPage is registered with MyVM
}
catch (Exception ex)
{
string mess = ex.Message;
}
My work-around is to add an event handler to the code-behind.
<DatePicker x:Name="ProcessStartDate" Format="D" MinimumDate="01/01/2000" DateSelected="OnStartDateSelected"
So now my code-behind has a handler:
void OnStartDateSelected(object sender, DateChangedEventArgs args)
{
SampleDetailsViewModel vm = BindingContext as SampleDetailsViewModel;
vm.SelectedStartDate = args.NewDate;
}
I have a work-around for this page, But I don't want put code in the code-behind. This breaks the MVVM standard that I've managed to maintain on the other seven pages of the app. Am I Binding improperly with the DatePicker?
When Binding SelectedStartDate, you are not initializing it, making it binding to a null, because you have set the Binding Mode to "TwoWay".
Here you can find the various types of binding modes, quoting:
Causes changes to either the source property or the target property to
automatically update the other. This type of binding is appropriate
for editable forms or other fully-interactive UI scenarios.
a solution would be something like this (if you wanna keep the TwoWay mode, and dont mind starting with an default selected):
private DateTime selectedStartDate = DateTime.Now;
Or
Making the binding mode to "OneWayToSource", this makes updates to the binding source without, and not the target (remember that this way you can't change the selected date from the binding, only the datepicker can).
Updates the source property when the target property changes.
Or
If you wanna keep the TwoWay Mode and not having a default date selected, the way you did with code behind is a nice workaround.
I have a UserControl that has a CheckBox on it. When I consume the UserControl on my main XAML page, I'd like to TwoWay bind a property on the control to a property on my ViewModel e.g.
<myUserControl BtnIsBlacklisted="{Binding IsBlacklisted, Mode=TwoWay}" />
When IsBlacklisted changes, I'd like my checkbox to change too and vice-versa.
Here is what I have,
public static readonly DependencyProperty BtnIsBlacklistedProperty =
DependencyProperty.Register("BtnIsBlacklisted",
typeof(bool),
typeof(MyUserControl),
new PropertyMetadata(false, new
PropertyChangedCallback(BtnIsBlacklistedPropertyChanged))
);
private static void BtnIsBlacklistedPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// ... do something here ...
}
public bool BtnIsBlacklisted
{
get { return (bool)GetValue(BtnIsBlacklistedProperty); }
set { SetValue(BtnIsBlacklistedProperty, value); }
}
My UserControl has this for the CheckBox,
<CheckBox x:Name="myCheckBox"
...
IsChecked="{Binding Path=BtnIsBlacklisted,
ElementName=UserControl,
Converter={StaticResource BoolToNotBool},
Mode=TwoWay}" />
The property on my ViewModel object is as follows,
public bool IsBlacklisted
{
get
{
return App.VM.BlacklistedRetailers.Contains(this.Retailer);
}
set
{
if (value)
{
App.VM.BlacklistedRetailers.Add(this.Retailer);
}
else
{
while (App.VM.BlacklistedRetailers.Contains(this.Retailer))
{
App.VM.BlacklistedRetailers.Remove(this.Retailer);
}
}
this.NotifyPropertyChanged("IsBlacklisted");
}
}
The only way BlacklistedRetailers changes is through the set method above so there is no need to trigger a NotifyPropertyChanged from there ...
I have tried many of the suggestions in other questions i.e.
using a dependency property
including Mode=TwoWay
Binding on the UserControl using a self-referencing DataContext set on the containing grid (this does not work either).
however none of these have worked.
Some final notes:
This is for a Windows Phone 7.5 project
Edit: One way binding doe not work either, it seems it there is a problem binding to the UserControl's own properties
An ElementName Binding matches against x:Name values which are in the same name scope as the element on which the binding is being set. There's not enough of the code shown to tell but you're using "UserControl" which I'm guessing is not set as the name of the element, but is being used to try and match the type. The ElementName also might not be able to resolve if the CheckBox is declared inside a template.