Подтвердить что ты не робот

Сортировка и группировка UWP ObservableCollection

В приложениях UWP, как вы можете группировать и сортировать ObservableCollection и сохранять все уведомления в реальном времени?

В большинстве простых примеров UWP, которые я видел, обычно существует ViewModel, который предоставляет ObservableCollection, который затем привязан к ListView в представлении. Когда элементы добавляются или удаляются из ObservableCollection, ListView автоматически отражает изменения, реагируя на уведомления INotifyCollectionChanged. Все это прекрасно работает в случае несортированного или негруппового ObservableCollection, но если сбор необходимо отсортировать или сгруппировать, похоже, нет очевидного способа сохранить уведомления об обновлениях. Что еще, изменение порядка сортировки или группы на лету, похоже, бросает серьезные проблемы с реализацией.

++

Возьмите сценарий, в котором у вас есть существующий бэкэнд datacache, который предоставляет ObservableCollection очень простого класса Contact.

public class Contact
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string State { get; set; }
}

Этот ObservableCollection изменяется со временем, и мы хотим представить в режиме реального времени сгруппированный и отсортированный список в представлении, которое обновляет в ответ на изменения в данных. Мы также хотим дать пользователю возможность переключаться между LastName и State на лету.

++

В мире WPF это относительно тривиально. Мы можем создать простой ViewModel, ссылающийся на datacache, который представляет коллекцию контактов кэша as-is.

public class WpfViewModel 
{
    public WpfViewModel()
    {
        _cache = GetCache();
    }

    Cache _cache;

    public ObservableCollection<Contact> Contacts
    {
        get { return _cache.Contacts; }
    }
}

Затем мы можем привязать это к представлению, где мы реализуем определения CollectionViewSource и Sort и Group в качестве ресурсов XAML.

<Window .....
   xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase">

   <Window.DataContext>
      <local:WpfViewModel />
   </Window.DataContext>

    <Window.Resources>
        <CollectionViewSource x:Key="cvs" Source="{Binding Contacts}" />
        <PropertyGroupDescription x:Key="stategroup" PropertyName="State" />
        <PropertyGroupDescription x:Key="initialgroup" PropertyName="LastName[0]" />
        <scm:SortDescription x:Key="statesort" PropertyName="State" Direction="Ascending" />
        <scm:SortDescription x:Key="lastsort" PropertyName="LastName" Direction="Ascending" />
        <scm:SortDescription x:Key="firstsort" PropertyName="FirstName" Direction="Ascending" />
    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <ListView ItemsSource="{Binding Source={StaticResource cvs}}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="100" />
                            <ColumnDefinition Width="100" />
                            <ColumnDefinition Width="*" />
                        </Grid.ColumnDefinitions>
                        <TextBlock Text="{Binding LastName}" />
                        <TextBlock Text="{Binding FirstName}" Grid.Column="1" />
                        <TextBlock Text="{Binding State}" Grid.Column="2" />
                    </Grid>
                </DataTemplate>
            </ListView.ItemTemplate>
            <ListView.GroupStyle>
                <GroupStyle>
                    <GroupStyle.HeaderTemplate>
                        <DataTemplate>
                            <Grid Background="Gainsboro">
                                <TextBlock FontWeight="Bold" 
                                           FontSize="14" 
                                           Margin="10,2"
                                           Text="{Binding Name}"/>
                            </Grid>
                        </DataTemplate>
                    </GroupStyle.HeaderTemplate>
                </GroupStyle>
            </ListView.GroupStyle>
        </ListView>

        <StackPanel Orientation="Horizontal" Grid.Row="1">
            <Button Content="Group By Initial" Click="InitialGroupClick" />
            <Button Content="Group By State" Click="StateGroupClick" />
        </StackPanel>

    </Grid>
</Window>

Затем, когда пользователь нажимает на кнопки GroupBy в нижней части окна, мы можем группировать и сортировать "на лету" в коде.

private void InitialGroupClick(object sender, RoutedEventArgs e)
{
     var cvs = FindResource("cvs") as CollectionViewSource;
     var initialGroup = (PropertyGroupDescription)FindResource("initialgroup");
     var firstSort = (SortDescription)FindResource("firstsort");
     var lastSort = (SortDescription)FindResource("lastsort");

     using (cvs.DeferRefresh())
     {
         cvs.GroupDescriptions.Clear();
         cvs.SortDescriptions.Clear();
         cvs.GroupDescriptions.Add(initialGroup);
         cvs.SortDescriptions.Add(lastSort);
         cvs.SortDescriptions.Add(firstSort);
     }
}

private void StateGroupClick(object sender, RoutedEventArgs e)
{
     var cvs = FindResource("cvs") as CollectionViewSource;
     var stateGroup = (PropertyGroupDescription)FindResource("stategroup");
     var stateSort = (SortDescription)FindResource("statesort");
     var lastSort = (SortDescription)FindResource("lastsort");
     var firstSort = (SortDescription)FindResource("firstsort");

     using (cvs.DeferRefresh())
     {
         cvs.GroupDescriptions.Clear();
         cvs.SortDescriptions.Clear();
         cvs.GroupDescriptions.Add(stateGroup);
         cvs.SortDescriptions.Add(stateSort);
         cvs.SortDescriptions.Add(lastSort);
         cvs.SortDescriptions.Add(firstSort);
     }
}

Все это прекрасно работает, и элементы обновляются автоматически по мере изменения коллекции кэша данных. Группировка и выбор Listview остается неизменной благодаря изменениям коллекции, а новые элементы контактов правильно группируются. Группирование может быть заменено пользователем State и LastName начальным пользователем во время выполнения.

++

В мире UWP CollectionViewSource больше не имеет коллекций GroupDescription и SortDescriptions, а сортировка/группировка должна выполняться на уровне ViewModel. Самый близкий подход к работоспособному решению, который я нашел, находится в соответствии с образцом пакета Microsoft в

https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/XamlListView

и этой статьи

http://motzcod.es/post/94643411707/enhancing-xamarinforms-listview-with-grouping

где ViewModel группирует ObservableCollection с помощью Linq и представляет его представлению в виде ObservableCollection сгруппированных элементов

public ObservableCollection<GroupInfoList> GroupedContacts
{
    ObservableCollection<GroupInfoList> groups = new ObservableCollection<GroupInfoList>();

    var query = from item in _cache.Contacts
                group item by item.LastName[0] into g
                orderby g.Key
                select new { GroupName = g.Key, Items = g };

    foreach (var g in query)
    {
         GroupInfoList info = new GroupInfoList();
         info.Key = g.GroupName;
         foreach (var item in g.Items)
         {
             info.Add(item);
         }
         groups.Add(info);
    }

    return groups;
}

где GroupInfoList определяется как

public class GroupInfoList : List<object>
{
   public object Key { get; set; }
}

Это, по крайней мере, дает нам сгруппированную коллекцию, отображаемую в представлении, но обновления коллекции datacache больше не отражаются в режиме реального времени. Мы могли бы захватить событие CollectionChanged для данных datacache и использовать его в модели view, чтобы обновить коллекцию GroupedContacts, но это создает новую коллекцию для каждого изменения в datacache, заставляя ListView мерцать и reset выбор и т.д., Который явно субоптимален.

Кроме того, для замены группировки "на лету" требуется совершенно отдельный ObservableCollection сгруппированных элементов для каждого сценария группировки, а привязка элемента ListView ItemSource должна быть заменена во время выполнения.

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

Кто-нибудь знает, как это сделать правильно?

4b9b3361

Ответ 1

Я начал собирать библиотеку под названием GroupedObservableCollection, которая делает что-то в этом направлении для одного из моих приложений.

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

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

Ответ 2

Наилучшее усилие до сих пор использует следующий вспомогательный класс ObservableGroupingCollection

public class ObservableGroupingCollection<K, T> where K : IComparable
{
    public ObservableGroupingCollection(ObservableCollection<T> collection)
    {
        _rootCollection = collection;
        _rootCollection.CollectionChanged += _rootCollection_CollectionChanged;
    }

    ObservableCollection<T> _rootCollection;
    private void _rootCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        HandleCollectionChanged(e);
    }

    ObservableCollection<Grouping<K, T>> _items;
    public ObservableCollection<Grouping<K, T>> Items
    {
        get { return _items; }
    }

    IComparer<T> _sortOrder;
    Func<T, K> _groupFunction;

    public void ArrangeItems(IComparer<T> sortorder, Func<T, K> group)
    {
        _sortOrder = sortorder;
        _groupFunction = group;

        var temp = _rootCollection
            .OrderBy(i => i, _sortOrder)
            .GroupBy(_groupFunction)
            .ToList()
            .Select(g => new Grouping<K, T>(g.Key, g));

        _items = new ObservableCollection<Grouping<K, T>>(temp);

    }

    private void HandleCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == NotifyCollectionChangedAction.Add)
        {
            var item = (T)(e.NewItems[0]);
            var value = _groupFunction.Invoke(item);

            // find matching group if exists
            var existingGroup = _items.FirstOrDefault(g => g.Key.Equals(value));

            if (existingGroup == null)
            {
                var newlist = new List<T>();
                newlist.Add(item);

                // find first group where Key is greater than this key
                var insertBefore = _items.FirstOrDefault(g => ((g.Key).CompareTo(value)) > 0);
                if (insertBefore == null)
                {
                    // not found - add new group to end of list
                    _items.Add(new Grouping<K, T>(value, newlist));
                }
                else
                {
                    // insert new group at this index
                    _items.Insert(_items.IndexOf(insertBefore), new Grouping<K, T>(value, newlist));
                }
            }
            else
            {
                // find index to insert new item in existing group
                int index = existingGroup.ToList().BinarySearch(item, _sortOrder);
                if (index < 0)
                {
                    existingGroup.Insert(~index, item);
                }
            }
        }
        else if (e.Action == NotifyCollectionChangedAction.Remove)
        {
            var item = (T)(e.OldItems[0]);
            var value = _groupFunction.Invoke(item);

            var existingGroup = _items.FirstOrDefault(g => g.Key.Equals(value));

            if (existingGroup != null)
            {
                // find existing item and remove
                var targetIndex = existingGroup.IndexOf(item);
                existingGroup.RemoveAt(targetIndex);

                // remove group if zero items
                if (existingGroup.Count == 0)
                {
                    _items.Remove(existingGroup);
                }
            }
        }

    }
}

где общий класс группировки (который сам предоставляет ObservableCollection) исходит из этой статьи

http://motzcod.es/post/94643411707/enhancing-xamarinforms-listview-with-grouping

Чтобы сделать рабочую демонстрацию: -

В новом приложении UWP Blank добавьте выше класс ObservableGroupingCollection. Затем добавьте еще один файл класса в том же пространстве имен и добавьте все следующие классы

// Data models

public class Contact
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string State { get; set; }
}

public class DataPool
{
    public static string GenerateFirstName(Random random)
    {
        List<string> names = new List<string>() { "Lilly", "Mukhtar", "Sophie", "Femke", "Abdul-Rafi", "Mariana", "Aarif", "Sara", "Ibadah", "Fakhr", "Ilene", "Sardar", "Hanna", "Julie", "Iain", "Natalia", "Henrik", "Rasa", "Quentin", "Gadi", "Pernille", "Ishtar", "Jimmy", "Justine", "Lale", "Elize", "Randy", "Roshanara", "Rajab", "Marcus", "Mark", "Alima", "Francisco", "Thaqib", "Andreas", "Marianna", "Amalie", "Rodney", "Dena", "Amar", "Anna", "Nasreen", "Reema", "Tomas", "Filipa", "Frank", "Bari'ah", "Parvaiz", "Jibran", "Tomas", "Elli", "Carlos", "Diego", "Henrik", "Aruna", "Vahid", "Eliana", "Roxanne", "Amanda", "Ingrid", "Wesley", "Malika", "Basim", "Eisa", "Alina", "Andreas", "Deeba", "Diya", "Parveen", "Bakr", "Celine", "Daniel", "Mattheus", "Edmee", "Hedda", "Maria", "Maja", "Alhasan", "Alina", "Hedda", "Vanja", "Robin", "Victor", "Aaftab", "Guilherme", "Maria", "Kai", "Sabien", "Abdel", "Jason", "Bahaar", "Vasco", "Jibran", "Parsa", "Catalina", "Fouad", "Colette", "John", "Fred", "James", "Harry", "Ben", "Steven", "Philip", "Dougal", "Jasper", "Elliott", "Charles", "Gerty", "Sarah", "Sonya", "Svetlana", "Dita", "Karen", "Christine", "Angela", "Heather", "Spence", "Graham", "David", "Bernie", "Darren", "Lester", "Vince", "Colin", "Bernhard", "Dieter", "Norman", "William", "Nigel", "Nick", "Nikki", "Trent", "Devon", "Steven", "Eric", "Derek", "Raymond", "Craig" };
        return names[random.Next(0, names.Count)];
    }
    public static string GenerateLastName(Random random)
    {
        List<string> lastnames = new List<string>() { "Carlson", "Attia", "Quincey", "Hollenberg", "Khoury", "Araujo", "Hakimi", "Seegers", "Abadi", "Krommenhoek", "Siavashi", "Kvistad", "Vanderslik", "Fernandes", "Dehmas", "Sheibani", "Laamers", "Batlouni", "Lyngvær", "Oveisi", "Veenhuizen", "Gardenier", "Siavashi", "Mutlu", "Karzai", "Mousavi", "Natsheh", "Nevland", "Lægreid", "Bishara", "Cunha", "Hotaki", "Kyvik", "Cardoso", "Pilskog", "Pennekamp", "Nuijten", "Bettar", "Borsboom", "Skistad", "Asef", "Sayegh", "Sousa", "Miyamoto", "Medeiros", "Kregel", "Shamoun", "Behzadi", "Kuzbari", "Ferreira", "Barros", "Fernandes", "Xuan", "Formosa", "Nolette", "Shahrestaani", "Correla", "Amiri", "Sousa", "Fretheim", "Van", "Hamade", "Baba", "Mustafa", "Bishara", "Formo", "Hemmati", "Nader", "Hatami", "Natsheh", "Langen", "Maloof", "Patel", "Berger", "Ostrem", "Bardsen", "Kramer", "Bekken", "Salcedo", "Holter", "Nader", "Bettar", "Georgsen", "Cuninho", "Zardooz", "Araujo", "Batalha", "Antunes", "Vanderhoorn", "Srivastava", "Trotter", "Siavashi", "Montes", "Sherzai", "Vanderschans", "Neves", "Sarraf", "Kuiters", "Hestoe", "Cornwall", "Paisley", "Cooper", "Jakoby", "Smith", "Davies", "Jonas", "Bowers", "Fernandez", "Perez", "Black", "White", "Keller", "Hernandes", "Clinton", "Merryweather", "Freeman", "Anguillar", "Goodman", "Hardcastle", "Emmott", "Kirkby", "Thatcher", "Jamieson", "Spender", "Harte", "Pinkman", "Winterman", "Knight", "Taylor", "Wentworth", "Manners", "Walker", "McPherson", "Elder", "McDonald", "Macintosh", "Decker", "Takahashi", "Wagoner" };
        return lastnames[random.Next(0, lastnames.Count)];
    }
    public static string GenerateState(Random random)
    {
        List<string> states = new List<string>() { "Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut", "Delaware", "District Of Columbia", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky", "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota", "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire", "New Jersey", "New Mexico", "New York", "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", "Pennsylvania", "Rhode Island", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", "Vermont", "Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming" };
        return states[random.Next(0, states.Count)];
    }
}

public class Cache
{
    public Cache()
    {
        InitializeCacheData();
        SimulateLiveChanges(new TimeSpan(0, 0, 1));
    }

    public ObservableCollection<Contact> Contacts { get; set; }

    private static Random rnd = new Random();

    private void InitializeCacheData()
    {
        Contacts = new ObservableCollection<Contact>();

        var i = 0;
        while (i < 5)
        {
            Contacts.Add(new Contact()
            {
                FirstName = DataPool.GenerateFirstName(rnd),
                LastName = DataPool.GenerateLastName(rnd),
                State = DataPool.GenerateState(rnd)
            });

            i++;
        }
    }

    private async void SimulateLiveChanges(TimeSpan MyInterval)
    {
        double MyIntervalSeconds = MyInterval.TotalSeconds;
        while (true)
        {
            await Task.Delay(MyInterval);

            //int addOrRemove = rnd.Next(1, 10);
            //if (addOrRemove > 3)
            //{
            // add item
            Contacts.Add(new Contact()
            {
                FirstName = DataPool.GenerateFirstName(rnd),
                LastName = DataPool.GenerateLastName(rnd),
                State = DataPool.GenerateState(rnd)
            });
            //}
            //else
            //{
            //    // remove random item
            //    if (Contacts.Count > 0)
            //    {
            //        Contacts.RemoveAt(rnd.Next(0, Contacts.Count - 1));
            //    }
            //}
        }
    }

}

// ViewModel

public class ViewModel : BaseViewModel
{       
    public ViewModel()
    {
        _groupingCollection = new ObservableGroupingCollection<string, Contact>(new Cache().Contacts);
        _groupingCollection.ArrangeItems(new StateSorter(), (x => x.State));
        NotifyPropertyChanged("GroupedContacts");

    }

    ObservableGroupingCollection<string, Contact> _groupingCollection;
    public ObservableCollection<Grouping<string, Contact>> GroupedContacts
    {
        get
        {
            return _groupingCollection.Items;
        }
    }

    // swap grouping commands

    private ICommand _groupByStateCommand;
    public ICommand GroupByStateCommand
    {
        get
        {
            if (_groupByStateCommand == null)
            {
                _groupByStateCommand = new RelayCommand(
                    param => GroupByState(),
                    param => true);
            }
            return _groupByStateCommand;
        }
    }
    private void GroupByState()
    {
        _groupingCollection.ArrangeItems(new StateSorter(), (x => x.State));
        NotifyPropertyChanged("GroupedContacts");
    }

    private ICommand _groupByNameCommand;
    public ICommand GroupByNameCommand
    {
        get
        {
            if (_groupByNameCommand == null)
            {
                _groupByNameCommand = new RelayCommand(
                    param => GroupByName(),
                    param => true);
            }
            return _groupByNameCommand;
        }
    }
    private void GroupByName()
    {
        _groupingCollection.ArrangeItems(new NameSorter(), (x => x.LastName.First().ToString()));
        NotifyPropertyChanged("GroupedContacts");
    }

}

// View Model helpers

public class BaseViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

public class RelayCommand : ICommand
{
    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;

    public RelayCommand(Action<object> execute)
        : this(execute, null)
    {

    }

    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");

        _execute = execute;
        _canExecute = canExecute;

    }

    public bool CanExecute(object parameter)
    {
        return _canExecute == null ? true : _canExecute(parameter);
    }

    public event EventHandler CanExecuteChanged
    {
        add { } 
        remove { } 
    }

    public void Execute(object parameter)
    {
        _execute(parameter);
    }

}

// Sorter classes

public class NameSorter : Comparer<Contact>
{
    public override int Compare(Contact x, Contact y)
    {
        int result = x.LastName.First().CompareTo(y.LastName.First());

        if (result != 0)
        {
            return result;
        }
        else
        {
            result = x.LastName.CompareTo(y.LastName);

            if (result != 0)
            {
                return result;
            }
            else
            {
                return x.FirstName.CompareTo(y.FirstName);
            }
        }
    }
}

public class StateSorter : Comparer<Contact>
{
    public override int Compare(Contact x, Contact y)
    {
        int result = x.State.CompareTo(y.State);

        if (result != 0)
        {
            return result;
        }
        else
        {
            result = x.LastName.CompareTo(y.LastName);

            if (result != 0)
            {
                return result;
            }
            else
            {
                return x.FirstName.CompareTo(y.FirstName);
            }
        }
    }
}

// Grouping class 
// credit
// http://motzcod.es/post/94643411707/enhancing-xamarinforms-listview-with-grouping

public class Grouping<K, T> : ObservableCollection<T>
{
    public K Key { get; private set; }

    public Grouping(K key, IEnumerable<T> items)
    {
        Key = key;
        foreach (var item in items)
        {
            this.Items.Add(item);
        }
    }
}

Наконец, отредактируйте MainPage следующим образом

  <Page.DataContext>
        <local:ViewModel />
    </Page.DataContext>

    <Page.Resources>
        <CollectionViewSource 
            x:Key="cvs" 
            Source="{Binding GroupedContacts}" 
            IsSourceGrouped="True" />
    </Page.Resources>

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <ListView ItemsSource="{Binding Source={StaticResource cvs}}"
                  x:Name="targetListBox">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="100" />
                            <ColumnDefinition Width="100" />
                            <ColumnDefinition Width="*" />
                        </Grid.ColumnDefinitions>

                        <TextBlock Text="{Binding LastName}" />
                        <TextBlock Text="{Binding FirstName}" Grid.Column="1" />
                        <TextBlock Text="{Binding State}" Grid.Column="2" HorizontalAlignment="Right" />
                    </Grid>
                </DataTemplate>
            </ListView.ItemTemplate>
            <ListView.GroupStyle>
                <GroupStyle>
                    <GroupStyle.HeaderTemplate>
                        <DataTemplate>
                            <Grid Background="Gainsboro">
                                <TextBlock FontWeight="Bold" 
                                           FontSize="14" 
                                           Margin="10,2"
                                           Text="{Binding Key}"/>
                            </Grid>
                        </DataTemplate>
                    </GroupStyle.HeaderTemplate>
                </GroupStyle>
            </ListView.GroupStyle>
        </ListView>

        <StackPanel Orientation="Horizontal" Grid.Row="1">
            <Button Content="Group By Initial" Command="{Binding GroupByNameCommand}" />
            <Button Content="Group By State" Command="{Binding GroupByStateCommand}" />
        </StackPanel>
    </Grid>

Метод HandleCollectionChanged обрабатывает только Add/Remove и будет разбиваться, если параметр NotifyCollectionChangedEventArgs содержит несколько элементов (существующий класс ObservableCollection уведомляет об изменениях по одному за раз)

Итак, все работает нормально, но все это выглядит как хакка.

Предложения по улучшению приветствуются.