WinUI: Completed callback for UIElement.Transitions property - c#

As the title says, is there a way to be notified when the transition in TransitionCollection that is assigned to UIElement.Transitions is completed?
It would be great if a simple sample code block is provided. Thanks in advance!
Transition class: https://learn.microsoft.com/en-us/uwp/api/windows.ui.xaml.media.animation.transition?view=winrt-22621
UIElement.Transitions property: https://learn.microsoft.com/en-us/uwp/api/windows.ui.xaml.uielement.transitions?view=winrt-22621
PaneThemeTransition transition = new PaneThemeTransition();
Transitions = new TransitionCollection { transition };

Related

System.ArgumentOutOfRangeException: 'Index must be within the bounds of the List. when adding a new item to ObservableCollection

This happens on a Xamarin.Forms app.
It is a simple app about making lists.
I got two pages: one with the lists and another to show the list's items. The error happens on the latter when I try to add a new item to an ObservableCollection.
This is a simplified version of the ViewModel:
public ObservableCollection<ListItem> Items { get; }
public Command AddItemCommand { get; }
public ItemsViewModel()
{
Items = new ObservableCollection<ListItem>();
AddItemCommand = new Command(OnAddItem);;
}
private async void OnAddItem()
{
await Device.InvokeOnMainThreadAsync(async () =>
{
if (string.IsNullOrEmpty(NewItemText))
return;
ListItem listITem = new ListItem()
{
ListId = _currentList.ListId,
Id = Guid.NewGuid().ToString(),
Text = NewItemText
};
Items.Add(listITem);
_currentList.ListItems.Add(listITem);
await DataStore.UpdateItemAsync(_currentList);
NewItemText = string.Empty;
});
}
The error happens on the Items.Add(listITem); call.
Tried wrapping the call on Device.InvokeOnMainThreadAsync with no luck.
The curious thing is it just happens on the second time I access the page.
The full project can be found on my GitHub:
https://github.com/JeffersonAmori/ListApp
It wouldn't surprise me if the error stopped happening if you lessen the chance for XForms to intervene before you've done the Add. (I'm hypothesizing that the underlying problem is a latent XForms bug.):
private async void OnAddItem()
{
// --- ASSUME we are already on MainThread. ---
// --- Avoid "await" (and any "..Invoke..Async") until after "Items.Add". ---
if (string.IsNullOrEmpty(NewItemText))
return;
ListItem listITem = new ListItem()
{
ListId = _currentList.ListId,
Id = Guid.NewGuid().ToString(),
Text = NewItemText
};
Items.Add(listITem);
_currentList.ListItems.Add(listITem);
NewItemText = string.Empty;
// ----- await Potentially slow operation(s) AFTER all quick UI calls. -----
await DataStore.UpdateItemAsync(_currentList);
}
CAVEAT #1: This doesn't fix any underlying problem, it just might make it happen less often. If its an XF problem, you might have to wrap your code in try..catch. In catch, determine if the item got added. If not try adding it again. Messy.
CAVEAT #2: This assumes OnAddItem is only called from MainThread. That will be true, if you never call it directly yourself - UI code will invoke the command on main thread.
CAVEAT #3: Assumes that all of the types involved (especially ListItem and _currentList.ListItems) are not UI types - they have no dependencies on Xamarin.Forms View classes.
I think the problem was the way the app was navigating between pages.
Before - when the error occurred - it was navigating using flyout routes via the flyout menu. Now I'm explicit registering the routes on code behind and pushing them into the navigation stack programmatically when the user clicks one of the items on the ColletcionView. This way I'm not reusing pages anymore.
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute(nameof(ItemsPage), typeof(ItemsPage));
}
and then
async private void OnListSelected(List list)
{
if (list == null)
return;
await Shell.Current.GoToAsync($"{nameof(ItemsPage)}?{nameof(ItemsViewModel.ListId)}={list.ListId}");
}
This way I seems to get the threads to behavior correctly and always get a new list when navigating between pages as they are pushed/popped into/from the navigation stack.
Bear in mind there's some guess work on this answer as I'm just getting back into Xamari.Forms and I'm fairly new to Shell.
Thanks again toolmakersteve for the insights.
Cheers.

Cannot perform action because the specified Storyboard was not applied to this object for interactive control

I have a heartbeat animation in a WPF storyboard, and when I receive heartrate sensor data I want to adjust the speed of the animation to match the heartrate. I'm calling storyboard.Begin(,true) at the start of the app. A little later I call storyboard.GetCurrentTime() which throws
Cannot perform action because the specified Storyboard was not applied to this object for interactive control
What am I doing wrong?
Here's a minimal stripped down example. It uses an embedded resource for image data.
Steps to reproduce.
Create a new dotnet core 3.1 WPF app and name it "AnimationWarning6"
Create a folder, "img", right click, create new bitmap, scribble something in mspaint, and save to "asdf.bmp"
In your solution, change the "Build Action" on the image to "Embedded Resource".
Since this is dotnet core, add a nuget package reference to System.Drawing.Common (4.7.0).
Open MainWindow.xaml and add <Image x:Name="ImageHeart"></Image> so it looks like
<Window x:Class="AnimationWarning6.MainWindow"
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"
xmlns:local="clr-namespace:AnimationWarning6"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Image x:Name="ImageHeart"></Image>
</Grid>
</Window>
Open MainWindow.xaml.cs and change it so that it looks like
using System;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
namespace AnimationWarning6
{
public partial class MainWindow : Window
{
private static readonly TimeSpan HeartGifNaturalDuration = new TimeSpan(0, 0, 1);
private Storyboard _heartStoryboard;
public MainWindow()
{
InitializeComponent();
// load embedded resource image into memory
var bm = new BitmapImage();
using (var img = System.Drawing.Image.FromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream("AnimationWarning6.img.asdf.bmp")))
{
using (var ms = new System.IO.MemoryStream())
{
img.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
ms.Position = 0;
bm.BeginInit();
bm.CacheOption = BitmapCacheOption.OnLoad;
bm.UriSource = null;
bm.StreamSource = ms;
bm.EndInit();
}
}
// create storyboard.
_heartStoryboard = new Storyboard();
_heartStoryboard.BeginTime = TimeSpan.Zero;
var oaukf = new ObjectAnimationUsingKeyFrames();
oaukf.BeginTime = TimeSpan.Zero;
TimeSpan current = TimeSpan.Zero;
// Add 10 DiscreteObjectKeyFrame to the ObjectAnimationUsingKeyFrames
for (int i=0; i<10; i++)
{
var kf = new DiscreteObjectKeyFrame(bm, current);
oaukf.KeyFrames.Add(kf);
current += new TimeSpan(0, 0, 0, 0, 500);
}
oaukf.Duration = current;
// Associate animation with the WPF image.Source
Storyboard.SetTarget(oaukf, ImageHeart);
Storyboard.SetTargetProperty(oaukf, new PropertyPath(Image.SourceProperty));
// Setup storyboard to play
_heartStoryboard.Children.Add(oaukf);
_heartStoryboard.RepeatBehavior = RepeatBehavior.Forever;
_heartStoryboard.Duration = HeartGifNaturalDuration;
_heartStoryboard.Name = "HeartStoryboard";
_heartStoryboard.Begin(ImageHeart, true);
// In my real app, receiving data from the sensor triggers an event and if
// enough time has elapsed the animation duration will be adjusted. I'm
// just mocking a 5 second timer for this example.
var t = new System.Timers.Timer(5000)
{
AutoReset = true,
};
t.Elapsed += (s, e) =>
{
int heartRate = 70; // <- dummy data for this example.
double scaleFactor = HeartGifNaturalDuration.TotalSeconds * (double)heartRate / 60.0;
var currentTime = _heartStoryboard.GetCurrentTime(); // <- this line throws
// Exception thrown: 'System.InvalidOperationException' in PresentationFramework.dll
// An exception of type 'System.InvalidOperationException' occurred in PresentationFramework.dll but was not handled in user code
// Cannot perform action because the specified Storyboard was not applied to this object for interactive control.
_heartStoryboard.RepeatBehavior = RepeatBehavior.Forever;
_heartStoryboard.Stop();
_heartStoryboard.SpeedRatio = scaleFactor;
_heartStoryboard.Begin(ImageHeart, true); // reset animation to apply new SpeedRatio
_heartStoryboard.Seek(currentTime);
};
// start timer
t.Start();
}
}
}
First off, I'm calling .Begin(ImageHeart, true), which should mark this as "controllable." (right?)
I tried calling .Stop() and .Stop(ImageHeart) and .Pause() and .Pause(ImageHeart) immediately prior to _heartStoryboard.GetCurrentTime() but that still throws, as mentioned here and here.
I think my problem is the same as this unanswered question (the sole answer seems like a comment) here.
This answer says
My problem went away when I explicitly defined a starting value for each animated property at keyframe 0
but I'm already doing that as far as I can tell.
While these two (duplicate, and both unanswered) questions, here and here, are about determining if a storyboard has already begun, I think this is actually the same problem I'm having. My current code uses
var currentTime = _heartStoryboard.GetCurrentTime(ImageHeart) ?? TimeSpan.Zero;
which throws the same warnings in the output console (System.Windows.Media.Animation Warning: 6) as mentioned in those two answers. This fixes the exception being thrown, but always sets currentTime to TimeSpan.Zero. This causes the animation to jerk when it resets instead of continuing at the same frame it was before the SpeedRatio change. So, back to the original question, why can't I call _heartStoryboard.GetCurrentTime()?
edit, source hunting
Exception stack trace gives
at System.Windows.Media.Animation.Storyboard.GetStoryboardClock(DependencyObject o, Boolean throwIfNull, InteractiveOperation operation)
at System.Windows.Media.Animation.Storyboard.GetCurrentTimeImpl(DependencyObject containingObject)
at System.Windows.Media.Animation.Storyboard.GetCurrentTime()
at AnimationWarning6.MainWindow.<.ctor>b__2_0(Object s, ElapsedEventArgs e) in C:\Users\tolos\code\csharp\AnimationWarning6\AnimationWarning6\MainWindow.xaml.cs:line 76
at System.Timers.Timer.MyTimerCallback(Object state)
Looking at the dotnet wpf source for GetStoryboardClock on github I guess it throws from
private Clock GetStoryboardClock(DependencyObject o, bool throwIfNull, InteractiveOperation operation)
{
Clock clock = null;
WeakReference clockReference = null;
HybridDictionary clocks = StoryboardClockTreesField.GetValue(o);
if (clocks != null)
{
clockReference = clocks[this] as WeakReference;
}
if (clockReference == null)
{
if (throwIfNull)
{
// This exception indicates that the storyboard has never been applied.
// We check the weak reference because the only way it can be null
// is if it had never been put in the dictionary.
throw new InvalidOperationException(SR.Get(SRID.Storyboard_NeverApplied));
...
If I break on the line before the exception, and execute in the immediate window
(Storyboard.StoryboardClockTreesField.GetValue((DependencyObject)ImageHeart)[_heartStoryboard] as WeakReference) == null
the result is false, yet the code in my application throws, so the result must be == null? I don't know how it can be null, because I can cast to Clock like the github source code
((Storyboard.StoryboardClockTreesField.GetValue((DependencyObject)ImageHeart)[_heartStoryboard] as WeakReference).Target as Clock).CurrentTime
gives TotalSeconds: 0.9651458
Storyboard was being called with the framework element as an argument with true to mark "controllable".
i.e.
_heartStoryboard.Begin(ImageHeart, true);
I think the problem is that it's assigning a clock to the ImageHeart, but not to the storyboard.
When GetCurrentTime is called without arguments, it looks up the clock associated with the storyboard (according to source file),
which is null in this case, because it was never set. The end result is that calling
_heartStoryboard.GetCurrentTime();
throws an exception:
Cannot perform action because the specified Storyboard was not applied to this object for interactive control
It seems like there's three options.
(1) Call GetCurrentTime with the ImageHeart. Somehow this only sort of works, and still results
in warning messages in the console. Animation jerks back to the first frame for some reason
when trying to continue at the correct time.
(2) Leave all the storyboard calls with explicit parameters, but somehow start the storyboard
clock. I'm not sure how to do this.
(3) Get rid of all parameters to storyboard calls, just call _heartStoryboard.Begin(). The comment for the method in source
says
Begins all animations underneath this storyboard, clock tree starts in "shared clocks" mode.
This also defaults isControllable to true.
This works for me, no more warnings, no more exceptions.

Howto let ReactiveCommands observe their own IsExecuting observable

I have several commands in my ViewModel and I want to have the CanExecute of each button to be bound to an observable busy which is defined as none of the buttons is currently executing.
The following is what I came up with, but obviously it runs into a NullReferenceException.
busy = Observable.CombineLatest(this.PlayCommand.IsExecuting, this.PauseCommand.IsExecuting, (play, pause) => play && pause);
this.PauseCommand = new ReactiveCommand(busy.Select(b => !b));
this.PlayCommand = new ReactiveCommand(busy.Select(b=> !b));
Also the CanExecuteObservable property on ReactiveCommand is readonly, so I need to define an IObservable before I initialize the commands.
Any ideas on how to solve this chicken and egg problem? A better way of observing busy state for a ViewModel (or a collection of ViewModels) would be appreciated also :-)
I would set up a proxy via using a Subject:
var areAllAvailable = new BehaviorSubject<bool>(true);
PauseCommand = new ReactiveCommand(areAllAvailable);
PlayCommand = new ReactiveCommand(areAllAvailable);
Observable.CombineLatest(PauseCommand.IsExecuting, PlayCommand.IsExecuting,
(pa,pl) => !(pa || pl))
.Subscribe(allAreAvailable);

Understanding why TPL Task can update UI withOUT FromCurrentSynchronizationContext

I am doing some TPL in VS2012, WPF with MVVM. I have a question that I think I know the answer to but wanted to know for sure. Consider this snippet:
TaskCanceller = new CancellationTokenSource();
TaskLoader = Task<object>.Factory.StartNew(() =>
{
//Test the current query
DataRepository dr = new DataRepository(DataMappingSelected);
string test = dr.TestMappingConnection();
if (test.IsNotNullEmpty())
throw new DataConnectionException(test);
//Create the CSV File
DataQueryCsvFile CsvFile = new DataQueryCsvFile();
CsvFile.FileName = IO.Path.GetFileName(FilePath);
CsvFile.FilePath = IO.Path.GetDirectoryName(FilePath);
CsvFile.DataMapping = DataMappingSelected;
CsvFile.DataBrowserQuery = DataQueryHolder;
//Allow for updates to the UI
CsvFile.PageExportComplete += (s, e) =>
{
if (TaskCanceller.IsCancellationRequested)
(s as DataQueryCsvFile).IsSaveCancellationRequested = true;
StatusData = String.Format("{0} of {1} Complete", e.ProgressCount, e.TotalCount);
StatusProgress = (100 * e.ProgressCount / e.TotalCount);
};
CsvFile.SaveFile();
return CsvFile;
});
I have a class DataQueryCsvFile. Its intent is to create a CSV text file based off a passed set of query parameters the results of which can be very large. So the export "paginates" the table produced by the query so it does not blow the users memory. Among its members is an Event called PageExportComplete which is called whenever a "Page" is written to a file - say 1000 records at a time. The code below uses this event to update a progress indicator on the UI.
The progress indicators (StatusData and StatusProgress) are declared in the VM with appropriate Notification to let the View know when they are changed. For example:
public string StatusData
{
get { return _StatusData; }
set { NotifySetProperty(ref _StatusData, value, () => StatusData); }
}
private string _StatusData;
Here is my question - as is, this works very well. But why since I did NOT declare the Task to run or update via the UI thread (FromCurrentSynchronizationContext) in a ContinueWith.
Is it because the MVVM pattern? In other words, because the properties being updated are local to the VM and because they have the notification to update the View and because of the lose coupling via bindings its works? Or am I just lucky due to the circumstances and I should go through the trouble of declaring a ContinueWith to update progress on the UI thread?
UI related stuff can only be updated from UI thread whereas any CLR property binded to UI can be updated from background thread, they don't have thread affinity issue.
Like you posted in your sample, you are only updating View model properties from background thread which is perfectly fine but if you try updating Progress bar text directly, it will fall miserably since progressBar is UI component and can only be updated from UI thread.
Say you have TextBlock binded to Name property in ViewModel class:
<TextBlock x:Name="txt" Text="{Binding Name}"/>
If you try to update text directly, you will get famous thread affinity issue:
Task.Factory.StartNew(() => txt.Text = "From background");
But in case you try to update ViewModel Name property, it will work fine since no UI stuff is access from background thread here:
ViewModelClass vm = DataContext as ViewModelClass;
Task.Factory.StartNew(() => vm.Name = "From background");

Setting button color in MonoTouch.Dialog

I have the following code:
[OnTap ("Account")]
[Alignment (UITextAlignment.Center)]
[Entry ("Create ScanDo! Account")]
public string Login;
And I'd like to set the Cell background color dynamically, based on the contents of another field and then after the button is clicked. Could anyone point me in a direction with some samples?
Thanks,
Rick
The answer I came up with:
btnLogin = new StyledStringElement("", delegate {Account();})
To define the object, add it to the RootElement, then:
btnLogin.BackgroundColor = UIColor.Green;
To set the color! This method let me set color, font, size and caption.
Great work Miguel, Thanks!
As you're adding the button to the root collection you can set the colour. Just as you set the elements of a section.
Root = new RootElement("First Section") {
new Section ("Test"){
new StyledStringElement("Login", delegate { Account(); })
{
BackgroundColor = UIColor.Green
}
}
}
I don't like to keep pimping my projects but in this case it is the best option for you.
Check out https://github.com/RobertKozak/MonoMobile.MVVM
My project started out as adding Databinding support to MonoTouch.Dialog but has grown into a much bigger framework that is much easier to use than MonoTouch.Dialog.
Using MonoMobile.MVVM the code to do what you want looks like this:
public class ButtonView : View, INotifyPropertyChanged
{
private UIColor ButtonColor = UIColor.Red;
[Button]
[Bind("ButtonColor", "BackgroundColor")]
public void Test()
{
ButtonColor = UIColor.Green;
PropertyChanged(this, new PropertyChangedEventArgs("ButtonColor"));
}
public event PropertyChangedEventHandler PropertyChanged = (s,e)=>{};
}
There are better ways to accomplish this using a proper ViewModel but this will work as listed (I tested it before I typed it in here.)
MonoMobile.MVVM is still in beta but there is enough there to get you going. It should be in full release status in the next couple of weeks after I finish up implementing INotifyCollectionChanged and adding a few more bindings for various Element properties.

Categories

Resources