John
John

Reputation: 827

.NET MAUI cyclic progress bar

I'm trying to make a cyclic progress bar using ProgressTo in combination with some bindings. I can get it to work by manually animating the progress bar without using ProgressTo, but cannot get it to work properly using ProgressTo (I cannot reset the progress).

Edit: removed code with errors, see below for code that will run but won't work properly

Once the progress bar goes to 100%, it stays there instead of cycling back.

I also haven't figured out how to change the ProgressTo parameters, which is the second part of the question, as the speed at which it fills during the second and later interations will be different from the first iteration.

When manually animating, which works but which I don't want to use if I can help it, the differences to the above are:

  1. _timer set to start now, and repeat every 10ms
  2. _timer just calls OnPropertyChanged()
  3. Current { get; } checks for expiry and rolls to new Segment if _current is expired.
  4. Current { private set; } does not call OnPropertyChanged()
  5. SegmentToProgress is properly implemented rather than a hard coded 0d.

--- Edit: looks like I made some mistakes when copying stuff here. See below for minimum reproduceable as requested in comment.

  1. Create a MAUI project in Visual studio. It has a CounterBtn with text Click me.
  2. Add SourceClass (see below).
  3. Modify MainPage.xaml to add: <ProgressBar x:Name="pb" /> and a label <Label x:Name="lbl" />
  4. Add RandomStringEnumerator (see below) or some other infinite enumerator of your choice.
  5. Edit MainPage.xaml.cs

Contents of SourceClass.cs

    public class SourceClass<T> : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler? PropertyChanged;

        private readonly DateTime _initial;
        private readonly TimeSpan _period;
        private Segment _current;
        private Timer _timer;
        private IEnumerator<T> _emitter;

        public SourceClass(DateTime initial, TimeSpan period, IEnumerator<T> emitter)
        {
            _initial = initial;
            _period = period;
            _emitter = emitter;
            _current = new Segment(_initial, _initial + _period, emitter.Current);
            _timer = new Timer((s) => { emitter.MoveNext(); Current = new Segment(Current.End, Current.End + _period, _emitter.Current); });
            _timer.Change(Current.End - DateTime.Now, period);

            OnPropertyChanged(nameof(Current));
        }

        public Segment Current
        {
            get => _current;
            private set
            {
                _current = value;
                OnPropertyChanged();
            }
        }

        public class Segment(DateTime Start, DateTime End, T Value)
        {
            public DateTime Start { get; } = Start;
            public DateTime End { get; } = End;
            public T Value = Value;
        }

        public void OnPropertyChanged([CallerMemberName] string name = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }

Contents of RandomStringEnumerator.cs

    internal class RandomStringEnumerator(int seed) : IEnumerator<string>
    {
        Random rng = new Random(seed);
        int initialSeed = seed;
        string? current;

        public string Current => current ?? string.Empty;

        object IEnumerator.Current => current ?? string.Empty;

        public void Dispose()
        {
            throw new NotImplementedException();
        }

        public bool MoveNext()
        {
            char[] chars = new char[rng.Next(5) + 5];
            for (int i = 0; i < chars.Length; i++)
            {
                chars[i] = (char)(0x20 + rng.Next(0x5e));
            }

            current = new string(chars);
            return true;
        }

        public void Reset()
        {
            current = null;
            rng = new Random(initialSeed);
        }
    }

Contents of MainPage.xaml.cs

    public partial class MainPage : ContentPage
    {
        private int count = 0;
        private readonly SourceClass<string> instanceOfSourceClass;
        private readonly int seed = 10;

        public MainPage()
        {
            InitializeComponent();
            IEnumerator<string> rse = new RandomStringEnumerator(seed);
            // discard first empty string element.
            rse.MoveNext();
            instanceOfSourceClass = new(DateTime.Now, TimeSpan.FromSeconds(30), rse);
        }

        protected override void OnAppearing()
        {
            base.OnAppearing();

            lbl.SetBinding(Label.TextProperty, new Binding("Current", source: instanceOfSourceClass, converter: segmentValue));
            pb.SetBinding(ProgressBar.ProgressProperty, new Binding("Current", source: instanceOfSourceClass, converter: segmentToProgress));
            pb.ProgressTo(1, (uint)(instanceOfSourceClass.Current.End - DateTime.Now).TotalMilliseconds, Easing.Linear);
        }

        private void OnCounterClicked(object sender, EventArgs e)
        {
            count++;

            if (count == 1)
                CounterBtn.Text = $"Clicked {count} time";
            else
                CounterBtn.Text = $"Clicked {count} times";

            SemanticScreenReader.Announce(CounterBtn.Text);
        }

        private static readonly SegmentToProgress segmentToProgress = new();
        internal class SegmentToProgress : IValueConverter
        {
            public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
            {
                // since callback will be called when expired, always reset it.
                return 0d;
            }

            public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
            {
                throw new NotImplementedException();
            }
        }

        private static readonly SegmentValue segmentValue = new();
        internal class SegmentValue : IValueConverter
        {
            public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
            {
                // since callback will be called when expired, always reset it.
                if (value is SourceClass<string>.Segment segment)
                {
                    return segment.Value;
                }

                return "could not find segment value";
            }

            public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
            {
                throw new NotImplementedException();
            }
        }

    }

What the above does:

  1. Shows a label with a random string and a progress bar displaying when it will change.
  2. When the progress bar reaches the end, the label changes, but the progress bar does NOT start again.

What it should do:

  1. Shows a label with a random string and a progress bar displaying when it will change.
  2. When the progress bar reaches the end, the label changes, and the progress bar starts from the start again to show when the label will next change.

Upvotes: 0

Views: 141

Answers (0)

Related Questions