Помощники на каждый день

в 10:03, , рубрики: .net, C#, wpf, разработка под windows

Некоторые задачи встречаются при написании кода так часто, что инструменты для их решения должны быть особенно удобны. И если стандартные инструменты языка/платформы разработки кажутся неудовлетворительными, то программист пишет свои решения, которые, в силу частоты использования, становятся его любимыми велосипедами. Поэтому первое, что он делает при смене языка/платформы, это ищет местные аналоги своих решений, либо возможности их воспроизвести.

Представляю свою коллекцию помощников для решения рутинных задач, сложившуюся после миграции с C++ Builder на C#, WPF.

Первая тройка

  public static class IComparableExtensions {
    public static T Minv<T>(this T value, T maxValue) where T : IComparable<T> {
      if (value.CompareTo(maxValue) >= 0) return maxValue;
      return value;
    }
    public static T Maxv<T>(this T value, T minValue) where T : IComparable<T> {
      if (value.CompareTo(minValue) <= 0) return minValue;
      return value;
    }
    public static T Limit<T>(this T value, T minValue, T maxValue) where T : IComparable<T> {
      if (value.CompareTo(minValue) <= 0) return minValue;
      if (value.CompareTo(maxValue) >= 0) return maxValue;
      return value;
    }
  }

Чем же мне оказались неудобными стандартные Math.Min и Math.Max?

1. Необходимостью использовать имя класса Math перед Min и Max. Это настолько раздражало при работе с кодом, содержащем большое количество этих функций, что я переопределял их внутри класса.

2. Необходимостью использовать имя класса Math перед Min и Max и неудобством из-за коцептуального ощущения, что этим функциям не место в этом классе. Какие-либо другие функции Math требовались мне только для работы с геометрией, а вот Min и Max — это счетчики и индексы, это наше всё! И описание методов выше очевидно это показывает.

3. Стандартная нотация функций Min и Max при большой вложенности кажется мне недостаточно читабельной. Я бы предпочел иметь бинарные операторы min и max. Определенные выше методы являются наибольшим приближением к желаемому.

И в заключение, мне всегда требуется усилие, чтобы сообразить, какую функцию нужно использовать для ограничения сверху или снизу. Метод Limit избавляет меня от этих затруднений.

В C# 6 можно написать using System.Math;, и использовать функции без префикса. Спасибо, но поздно.

StringMaker

Для отладки или выдачи в лог часто требуется просто перечислить некоторые значения через пробел.
И для этого можно написать метод вроде void OutDebug(params object[] args) с простой логикой внутри. Но когда классов с таким методом становится несколько, требуется другое решение.

  public class SM {
    StringBuilder Sb;
    SM() { Sb = new StringBuilder(); }
    SM Add(object value) {
      if (value==null) return this;
      var objects = value as IEnumerable;
      if (objects!=null) {
        foreach (var obj in objects) Add(obj);
      } else Sb.Append(value.ToString());
      return this; 
    }
    public override string ToString() { return Sb.ToString(); }
    public static implicit operator string(SM value) { 
      return value==null ? null : value.ToString(); 
    }
    public static SM operator +(SM a, object b) { return a.Add(b); }
    public static SM operator -(SM a, object b) { Sb.Append(' '); return Add(b); }
    public static SM New { get { return new SM(); } }
  }

  public static class IEnumerableExtensions {
    public static IEnumerable Sep(this IEnumerable objects, object separator) {
      bool first = true;
      foreach (var obj in objects) {
        if (first) first = false;
        else yield return separator;
        yield return obj;
      }
      yield break;
    }
  }

Используем:

  var sm = SM.New+"Числа"-1-2-3;
  var rr = new int[] { 1, 2, 3 }; 
  sm = sm+" ("+rr.Sep(", ")+')';
  Trace.WriteLine(sm);

В C# 6 появилась возможность вписывать аргументы внутрь строки. Спасибо, но поздно.

TimerTask

В среде, управляемой событиями, иногда возникает необходимость продолжить работу после обработки накопившихся событий. Обычно, для отображения промежуточных результатов работы.

В WPF это можно сделать непосредственно:

Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new Action(delegate { }));

А можно оформить код, как итератор, и исполнить его с помощью следующего класса:

  public class TimerTask {
    public bool IsPaused, IsCancelled;
    DateTimeOffset NextTime;
    TimeSpan Interval;
    Func<bool> Func;

    static DispatcherTimer Timer;
    static List<TimerTask> TaskList;

    TimerTask (double interval, double delay, Func<bool> func) {
      if (TaskList==null) {
        TaskList = new List<TimerTask>();
        Timer = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(0.02), IsEnabled = true };
        Timer.Tick += Timer_Tick;
      }
      TaskList.Add(this);
      Interval = TimeSpan.FromSeconds(interval);
      NextTime = DateTimeOffset.Now+TimeSpan.FromSeconds(delay);
      Func = func;
    }
    static void Timer_Tick(object sender, EventArgs ea) {
      int i = 0, cnt = TaskList.Count;
      while (i<cnt) {
        if (TaskList[i].IsCancelled) { TaskList.RemoveAt(i); cnt--; } 
        else { TaskList[i].Tick(); i++; }
      }
    }
    void Tick() {
      if (IsPaused || DateTimeOffset.Now<NextTime) return;
      IsCancelled = !Func();
      NextTime = DateTimeOffset.Now+Interval;
    }
    public static TimerTask DoOnce(Action action, double delay) {
      return new TimerTask(0, delay, () => { action(); return false; });
    }
    public static TimerTask DoForever(Action action, double interval, double delay) {
      return new TimerTask(interval, delay, () => { action(); return true; });
    }
    public static TimerTask DoWhile(Func<bool> func, double interval, double delay) {
      return new TimerTask(interval, delay, () => { return func(); });
    }
    public static TimerTask DoEach(IEnumerable<object> enumerable, double interval, double delay) {
      var enumerator = enumerable.GetEnumerator();
      return new TimerTask(interval, delay, () => { return enumerator.MoveNext(); });
    }
  }

Используем:

  public partial class MainWindow : Window {
    public MainWindow() {
      InitializeComponent();
      TimerTask.DoEach(Start(), 0, 0);
    }
    IEnumerable<object> Start() {
      Title = "Starting 1";
      yield return null;
      Starting1();
      Title = "Starting 2";
      yield return null;
      Starting2();
      Title = "Starting 3";
      yield return null;
      Starting3();
      Title = "Started";
      yield break;
    }
  }

Также этот класс может использоваться для реализации прогресс-диалогов.

Очевидным недостатком данного класса является то, что вызов метода, подобного ShowDialog, в одном из заданий, блокирует исполнение и всех других. Этого бы не было, если бы каждое задание имело собственный экземпляр DispatcherTimer.

IntRangeNotifyCollection

Этот класс решает задачу, которая точно не является частой, но бывает очень важной для программ, отображающих большое количество данных. Если ListBox с виртуализацией показывает лишь 40 записей из коллекции в 100 000, естественно решить, что настоящих записей достаточно иметь только эти 40. Также уведомлять контрол об изменениях в коллекции желательно только в случае необходимости.

То есть, в качестве ItemsSource нужно подставить не настоящую коллекцию, а какой-то другой класс. Самый легковесный, какой только может быть.

  public class IntRangeEnumerator : IEnumerator {
    int _Current, _Last;
    public IntRangeEnumerator(int count) : this(0, count) { }
    public IntRangeEnumerator(int start, int count) { _Current = start-1; _Last = start+count; }
    public object Current { get { return _Current; } }
    public bool MoveNext() { _Current++; return _Current<_Last; }
    public void Dispose() { }
    public void Reset() { }
  }

  public class IntRange : IList {
    int _Start, _Count;
    public IntRange(int count) : this(0, count) { }
    public IntRange(int start, int count) { _Start = start; _Count = count; }
    public int Count { get { return _Count; } }
    public IEnumerator GetEnumerator() { return new IntRangeEnumerator(_Start, _Count); }
    public object this[int index] { get { return _Start+index; } set { } }

Другие методы

    public bool IsSynchronized { get { return true; } }
    public object SyncRoot { get { return this; } }
    public void CopyTo(Array array, int index) {
      for (int i = 0; i<_Count; i++) array.SetValue(_Start+i, index+i);
    }
    public bool IsFixedSize { get { return true; } }
    public bool IsReadOnly { get { return true; } }
    public int Add(object value) { return 0; }
    public void Clear() { }
    public bool Contains(object value) {
      if (!(value is int)) return false;
      int i = (int)value;
      return i>=_Start && i<_Start+_Count; 
    }
    public int IndexOf(object value) {
      if (!(value is int)) return -1;
      int i = (int)value;
      return i>=_Start && i<_Start+_Count ? i-_Start : -1; 
    }
    public void Insert(int index, object value) { }
    public void Remove(object value) { }
    public void RemoveAt(int index) { }

  }

  public class IntRangeNotifyCollection : IEnumerable, INotifyCollectionChanged {
    int _Count;
    public event NotifyCollectionChangedEventHandler CollectionChanged;
    public IntRangeNotifyCollection() { }
    public IEnumerator GetEnumerator() { return new IntRangeEnumerator(_Count); }
    protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e) {
      if (CollectionChanged!=null) CollectionChanged(this, e);
    }
    public int Count { 
      get { return _Count; }
      set {
        if (value==_Count) return;
        NotifyCollectionChangedEventArgs e;
        if (value==0) {
          e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
        } else
        if (value>_Count) {
          e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, 
            new IntRange(_Count, value-_Count), _Count);
        } else {
          e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, 
            new IntRange(value, _Count-value), value);
        }
        _Count = value;
        OnCollectionChanged(e);
      }
    }
  }

Как затем связать индексы с записями? Это уже сильно зависит от задачи.

Публикации на эту тему на Хабре:
» Функциональность с Range в ObservableCollection
» Виртуализация данных в WPF

Автор: AmirYantimirov

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js