Страницы

Поиск по вопросам

Показаны сообщения с ярлыком binding. Показать все сообщения
Показаны сообщения с ярлыком binding. Показать все сообщения

суббота, 11 апреля 2020 г.

Core Data не сохраняется число

#macos #xcode #binding #coredata #objective_c

                    
Здравствуйте! Создал простенькую программку с поддержкой Core Data, типа Document–Based
app. Имеется текстовое поле, соединённое с arrayController'ом, который в свою очередь
соединён с Entity, у которой есть атрибут "name" типа "string" и атрибут "score" типа
"decimal". Введённая в текстовое поле строка сохраняется без проблем.
Также имеется другое текстовое поле: в него вводится число и после нажатия кнопки
"Go" значение из поля передаётся в метку. Метка соединена абсолютно точно так же как
и первое текстовое поле, но почему-то значение метки не сохраняется.
С чем это связано и как это исправить?
Для наглядности, проект можно скачать отсюда: https://www.dropbox.com/sh/kfp61xu3i4zj3kz/QCnPtlVqtE    


Ответы

Ответ 1



У вас проблема в том, что "запись" в базу не происходит. Изыскания показали, что данные из TextField записываются в базу только после переключения "фокуса" из этой TextField в другое место. А у вас получается, что фокусировки нет совсем. Попробуйте дополнить код "принудительно" записю данных в Базу. Например: - (IBAction)changeScore:(id)sender { [_scoreLabel setDoubleValue:[_scoreField doubleValue]]; id saveObj = [_arrayController selectedObjects][0]; double newDob = [_scoreField doubleValue]; NSNumber * decim = [NSNumber numberWithDouble:newDob]; [saveObj setValue:decim forKey:@"score"]; }

четверг, 9 апреля 2020 г.

Binding к this[enum] свойству экземпляра класса

#c_sharp #wpf #xaml #binding

                    
Есть enum

namespace namespace1.namespace2{
  enum PropertyObject 
  {
    A=1,
    B=2,
    ...
  }
}


и класс совершенно в другом namespace

public class MyClass
{
  public Object this[namespace1.namespace2.PropertyObject property]
  {
    get
    {
      return ...;
    }
  }
}


Список экземпляров всунут в ItemsSource ListView, есть также DataTemplate забитый
как ItemTemplate у ListView


  



я уверен, что данный PropertyObject у экземпяров установлен, однако пишет, что не
может достучаться до этого свойства, как создать валидный Binding к этим свойствам?

ошибка: System.Windows.Data Error: 40 : BindingExpression path error: '[]' property
not found on 'object' ''MyClass' (HashCode=55693485)'. BindingExpression:Path=this[namespace1.namespace2.PropertyObject.A];
DataItem='MyClass' (HashCode=55693485); target element is 'TextBlock' (Name=''); target
property is 'Text' (type 'String')
    


Ответы

Ответ 1



Вы используете неверный синтаксис для получения значения перечисления. В рамках XAML пространства имён в сборках (namespace в C#) и пространства имён в XML (xmlns в XML) задаются в шапке файла. При этом даже указание точного типа может не понадобиться: XAML достаточно умный, чтобы приобразовать строку к перечислению самостоятельно. Понятия "this" в биндингах тоже нет. Скорее всего, вам нужен или текущий контекст (Path='.' или просто Path=''), или текущий элемент (RelativeSource='{RelativeSource Self}' и прочие). Положим, у нас есть следующий код: namespace HakunaMatata { public partial class MainWindow { public MainWindow () { InitializeComponent(); DataContext = new Model(); } } public class Model { public string this [Other.Property prop] => prop.ToString() + " from Indexer"; } } namespace Other { public enum Property { Foo, Bar, } } Тогда все из способов обращения к индексатору будут работать корректно:

Ответ 2



Синтаксис Binding.Path - есть примеры разных привязок.

суббота, 7 марта 2020 г.

Асинхронная загрузка изображений без остановки ui wpf

#c_sharp #wpf #многопоточность #binding #async


Есть стэк, который биндится к списку изображений. 


    
        
            
                
            
        
        
            
                
                    
                
            
        
    



Как мне асинхронно подгружать в него изображения (Большой объем), чтобы ui не останавливался?
Пробовал в коллекцию через асинхронный метод добавлять, но поток все равно останавливается.
Во view добавлял объекту IsAsync, тоже не помогает.

private async void GenerateTop()
{
    Screenshots = new ObservableCollection();
    await Task.Run(() =>
    {
        LastAdded.Add(...)...
    }
}

    


Ответы

Ответ 1



Вы должны разделить модель и представление. Функции загрузки модели должны бежать в неосновном потоке, и там пусть загружают что угодно как угодно медленно. Представление (в случае, если вы используете MVVM, это ваша VM) получает от модели любым способом извещение о том, что данные загрузка произошла и есть новые данные для показа, и (в главном потоке!) обновляет VM-список изображений. Таким образом ваш UI не будет подвисать, ваш код будет будет асинхронным, а волосы — мягкими и шелковистыми. Если подвисает загрузка картинки, попробуйте и правда использовать IsAsync в привязке. Единственная тонкость — у вас сейчас IsAsync грузит асинхронно строку, а конверсия в картинку выполняется в UI-потоке. Попробуйте указать конвертер: class ImageSourceLoadingConverter : IValueConverter { public object Convert(object value, Type targetType, object p, CultureInfo ci) => new BitmapImage(new Uri((string)value)); public object ConvertBack(object value, Type targetType, object p, CultureInfo ci) => throw new NotSupportedException(); } в этом случае, кажется, конверсия будет производиться асинхронно.

Ответ 2



Сделайте обычный метод(не асинхронный). Заполните в нем всю коллекцию и попробуйте так: далее: private void GenerateTop() { Screenshots = new ObservableCollection(); LastAdded.Add(...)... }

пятница, 6 марта 2020 г.

Не могу разобраться ObservableCollection с ListBox

#c_sharp #net #wpf #binding


Проблема скорее всего в привязке. В ListBox не прибавляются элементы.



    
    
        
            
                
                    
                    
                
                
                
                    
                    
                    
                
            
        
    


    
    


Ответы

Ответ 1



Где вы находите такие способы создания коллекций?) Ну, исправим то что есть: Объект создается в ресурсе окна, соответственно добавление элементов в коллекцию должно происходить в конструкторе этого объекта. То что вы добавляете элементы в *.cs файле, это не совсем то, так как это не тот объект к которому вы привязываетесь.(привязка идет к объекту созданному в ресурсе). Идем дальше. Раз уж вы решили создать коллекцию унаследовавшись от нее самой, то добавляйте элементы унаследованным методом: public class MyList : ObservableCollection { public MyList() : base() { Add(new Person("1", "2", "3", null)); Add(new Person("1", "2", "3", null)); } } В принципе все, должно работать. Пару замечаний по вашему коду: public ObservableCollection MyData; Если вы будете привязываться к MyData то привязка не пройдет. Привязка работает только со свойствами, т.е. должно выглядеть так: public ObservableCollection MyData { get; set; } Как это делается обычно: В проекте должен быть класс VM, реализующий интерфейс INotifyPropertyChanged. Этот класс будет являться DataContext-oм для вашего окна(как задать DataContext). В этом классе и объявить список: public class MainVM : INotifyPropertyChanged { public ObservableCollection MyList { get; set; } public MainVM() { MyList = new ObservableCollection(); MyList.Add(new Person("1", "2", "3", null)); MyList.Add(new Person("1", "2", "3", null)); } //--------------------- //реализация интерфейса } //--------------------- После таких манипуляций, использовать привязку одно удовольствие:

понедельник, 24 февраля 2020 г.

Binding color to constants class

#c_sharp #wpf #binding #color #const


Не могу прибиндить цветовую константу, которая находится в другой dll-ке.

"/>


Студия ругается на плохой код и  не хочет ничего делать. И принимает только объект,
а стринг не хочет. Как я понял, нужен конвертер.

Есть ли стандартный, или самому писать? (если можно, пример).
    


Ответы

Ответ 1



Ссылки на статические свойства нужно давать не через Binding, а через x:Static: При этом у вас, разумеется, должно быть правильно объявлено пространство имён constants, что-то вроде xmlns:constants="clr-namespace:Namespace.In.Assembly;assembly=Your.Assembly.Name" Не забудьте убедиться, что ваша сборка с константами называется Your.Assembly.Name, в ней в пространстве имён Namespace.In.Assembly находится public-класс ColorConstants, у которого есть статическое свойство (а не поле!) Black, у которого хотя бы getter объявлен как public. Для случая, когда ваш x:Static обладает неправильным типом (у вас строка, а ожидается Color), вам нужно применить конвертер. Самый простой синтаксически способ — написать свой MarkupExtension в замену x:Static, который заодно конвертировал бы значение в нужный тип стандартным конвертером. Итак: public class SmartStaticExtension : StaticExtension { public SmartStaticExtension(string member) : base(member) { } public override object ProvideValue(IServiceProvider serviceProvider) { // получим значение стандартного x:Static object value = base.ProvideValue(serviceProvider); // попытаемся вычислить тип, к которому надо привести IProvideValueTarget target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget; if (target != null) { Type targetType = typeof(object); // если целевое свойство - DependencyProperty DependencyProperty dp = target.TargetProperty as DependencyProperty; if (dp != null) { targetType = dp.PropertyType; // берём тип у него } else { // целевое свойство - обыкновенное свойство, используем рефлексию PropertyInfo pi = target.TargetProperty as PropertyInfo; if (pi != null) targetType = pi.PropertyType; } // получаем стандартный конвертер: TypeConverter converter = TypeDescriptor.GetConverter(targetType); // и конвертируем value = converter.ConvertFrom(value); } return value; } } Имея такой конвертер, можно написать (разумеется, пространство имён local должно указывать туда, где определён SmartStaticExtension). Работает как со строками ("LightCyan") так и с RGB-константами ("#708090").

пятница, 14 февраля 2020 г.

Привязка TwoWay к List<int>

#c_sharp #wpf #xaml #binding


List Items { get; set; } = new List() { 1, 2, 3, 4, 5 };





   
       
          
       
   



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

p.s. не оборачивая int в класс.
    


Ответы

Ответ 1



Напрямую привязаться не удастся, но это не сильно большая проблема. Заводить обёртку вокруг int на уровне VM неправильно, поэтому применим конвертер. class EditableValueTypeConverter : IValueConverter where T : struct { class ItemWrapper { public ItemWrapper(IList container, int index) => (this.container, this.index) = (container, index); IList container; int index; public T Value { get => container[index]; set => container[index] = value; } } public object Convert(object value, Type targetType, object p, CultureInfo ci) { var list = (IList)value; return Enumerable.Range(0, list.Count) .Select(idx => new ItemWrapper(list, idx)) .ToList(); // или просто list.Select((t, idx) => new ItemWrapper(list, idx)).ToList() } public object ConvertBack(object value, Type targetType, object p, CultureInfo ci) => throw new NotSupportedException(); } class IntListConverter : EditableValueTypeConverter { } Теперь наш XAML выглядит как-то так: Результат:

четверг, 13 февраля 2020 г.

Wpf: привязка данных из объекта

#c_sharp #framework #wpf #net #binding


Есть отдельный класс Piple с кучей данных о человеке, я создаю объект этого класса
в MainWinow. В главном окне приложения у меня множество различных texbox, для этого
объекта (имя, фамилия и т.д.). Можно ли как-то использую Binding, сделать так, что
бы при изменение объекта изменялись бы данные в соответствующих textbox, и наоборот,
при изменение в textbox, изменялись бы соответствующие поля в Piple?

Вот код  класса Piple:

namespace WpfApplication4
{
    public class Piple 
     {          
        public string name;
        public string name2;
        public Piple()
        {   
            name="Иван";
            name2="Иванов";
        }    
    }
}


Код главнного класса C#:  

  namespace WpfApplication4
  {
        /// 
        /// Логика взаимодействия для MainWindow.xaml
        /// 
        public partial class MainWindow : Window
        {
            Piple chel = new Piple();
            public MainWindow()
            {
                InitializeComponent();
                this.DataContext = this;                    
            }
         }
}


Код окна на XAML


    

        


        

    



Нужно соответственно привязать данныйе из chel.name и chel.name2 к соответствующим
textbox. Подскажите можно ли это сделать с помощью Binding?
    


Ответы

Ответ 1



Привязка к полям класса в WPF невозможна, Вам нужны хотя бы свойства (property). Чтобы при изменении свойств класса новые значения отображались в контроле, нужно чтобы класс был наследником интерфейса INotifyPropertyChanged Вы сами биндинги-то не прописали у TextBox'ов. Все это должно выглядеть как-то так: public class People : INotifyPropertyChanged // Наследуемся от нужного интерфеса { // Ваши поля private string name, name2; public event PropertyChangedEventHandler PropertyChanged; // Событие, которое нужно вызывать при изменении // Для удобства обернем событие в метод с единственным параметром - имя изменяемого свойства public void RaisePropertyChanged(string propertyName) { // Если кто-то на него подписан, то вызывем его if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } // А тут будут свойства, в которые мы обернем поля public string Name { get { return name; } set { // Устанавливаем новое значение name = value; // Сообщаем всем, кто подписан на событие PropertyChanged, что поле изменилось Name RaisePropertyChanged("Name"); } } public string Name2 { get { return name2; } set { // Устанавливаем новое значение name2 = value; // Сообщаем всем, кто подписан на событие PropertyChanged, что поле изменилось Name2 RaisePropertyChanged("Name2"); } } } В коде контрола создаем экземпляр класса и прописываем DataContext (на самом деле DataContext можно прописать и в разметке контрола): public MainWindow() { // Создаем экземпляр нашего класса P = new People(){Name = "Ololosha", Name2 = "Trololosha"}; InitializeComponent(); // Устанавливаем как контекст данных DataContext = P; } А в разметке контрола прописываем TextBox'ы: Вот теперь оно будет работать так, как вы хотите. Еще наверное надо упоминуть о таком свойстве биндинга как UpdateSourceTrigger. Оно описывает когда надо изменить источник. По умолчанию у TextBox'а стоит значение LostFocus, т.е. изменение произойдет только когда TextBox потеряет фокус. Если Вы хотите, чтобы изменения происходили сразу же, то Вам нужно UpdateSourceTrigger установить в PropertyChanged: Я бы Вам посоветовал найти хорошую книжку по WPF. Вот тут есть довольно неплохой сайт о WPF (и не только о нем), по крайней мере мне в свое время он очень помог

Как связать TextBox.Text с свойством другого объекта (binding)

#c_sharp #wpf #binding


Мне нужно в одном TextBox связать его свйоство Text с одним свойством из Settings.
Т.е. при изменении TextBox.Text должно меняться поле в Settings и наоборот. Знаю точно
что можно при помощи Binding'а сделать, однако... похоже настолько туп, что не могу
найти элементарный гайд по байндингу. Пожалуйста, напишите простейший пример на основе
моей задачи, только без наворотов, что-бы понять можно было даже мне :)
    


Ответы

Ответ 1



Всё просто. Если пространство имён по умолчанию вашего приложения — MyApp, ваши настройки обычно находятся в пространстве имён MyApp.Settings. Вам по идее нужен объект MyApp.Settings.Default. Пускай свойство называется NumberOfDragons. Тогда делаем вот как: Что это означает? Мы определили пространство имён settings для XAML, чтобы оно ссылалось туда, где лежат ваши Settings. Мы в Binding'е указали объект, к которому происходит привязка: Source={x:Static settings:Settings.Default}. Это означает, что мы вызываем статический метод Settings.Default, и работаем с тем, что получили из него. (Source задаёт объект, к свойству которого мы привязываемся.) Указываем само свойство: Path=NumberOfDragons. (Path задаёт свойство объекта, к которому мы привязываемся.) Всё! Не забудьте в конце работы приложения сказать Settings.Default.Save();. В принципе, вы выбрали не самый лёгкий объект для Binding'а. Обычно привязываются не к какому-то статическому объекту, а к DataContext, в этом случае вам не надо указывать Source, оно подразумевается. Ну и имя параметра Path (только этого параметра!) можно не указывать: По поводу всяких продвинутых параметров Binding, вот есть краткая сводка (на английском): Шпаргалка по WPF Binding.

WPF. Нужно колонку заполнить ссылками

#c_sharp #wpf #xaml #binding #datagrid


Имеется источник данных и DataGrid. Нужно заполнить последнюю колонку ссылками, ссылки
будут однотипные примера somesite.ru/user?id=ID, где id берестся из ItemSource. Все
никак с Binding разобраться не могу.


        
            
            
            
            
            
            
                
                    
                        
                            
                        
                    
                
            
        
        
            

            
        
    

    


Ответы

Ответ 1



А что сложного? Создаёте конвертер: class UriConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { var id = (string)value; return new Uri("http://somesite.ru/user?id=" + id); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotSupportedException(); } } Затем, положите его в ресурсы вашего окна: И используйте для конвертации: Подписка на RequestNavigate нужна для того, чтобы работали ссылки. void OnRequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e) { var uri = e.Uri.AbsoluteUri; Task.Run(() => Process.Start(uri)); e.Handled = true; } Вроде бы всё, должно работать.

Ответ 2



А еще проще через форматирование вот как выглядит биндинг {Binding YourProperty, StringFormat='somesite.ru/user?id={0}'} Ну а в вашем случае просто {Binding StringFormat='somesite.ru/user?id={0}'} Короче все то же самое, что написал VladD, только без конвертера

среда, 22 января 2020 г.

Загрузка изображения в Image из бинарных данных

#c_sharp #wpf #изображения #binding


Доброго времени суток!

Необходимо загружать и выгружать данные из Image в бинарные данные, но не понимаю
как. Ранее использовал binding к ресурсам или физическому файлу на диске.

Что я делал (код писал по памяти, потому могут быть опечатки или некоторые не точности)?


Создал окно, в которое поместил Image Source="{Binding myImage, converter={x:static
StringToBitmapImageConverter}}" Как видно в свойстве myImage расположен string в формате
base64 (данных загружаются из XML) и при помощи конвертера я их привожу в формат класса
BitmapImage
Конвертер, если получает пустую строку, или не валидные данные, загружает из ресурсов
значок по умолчанию.
При клике по картинке, открывается диалог выбора файла, откуда я считываю его бинарное
представление и перевожу его в формат string Base64
После открытия файла - я не вижу картинку (она не отображается). Однако картинка
по умолчанию загружается корректно.


Не могу понять в чем проблема.

Попробую привести пример кода, связанного с проблемным местом (как я думаю)

Конвертер

public object Convert(object value, Type targetType, object parameter, CultureInfo
culture)
{
    if (value is string && targetType != null && targetType.Equals(typeof(ImageSource)))
    // здесь я экспериментировал с проверкой типа, по отладке понял, что от окна
приходит ImageSource
    {
        var bitmap = new BitmapImage();
        try
        {
            var buffer = System.Convert.FromBase64String(value as string);
            using (var ms = new MemoryStream())
            {
                // Здесь загрузка проходит, но изображение не появляется на экране
                ms.Write(buffer, 0, buffer.Length);
                ms.Seek(0, SeekOrigin.Begin);
                bitmap.BeginInit();
                bitmap.StreamSource = ms;
                bitmap.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
                bitmap.CacheOption = BitmapCacheOption.Default;
                bitmap.EndInit();
            }
        }
        catch (Exception ex)
        {
            Trace.WriteLine(ex.Message);
        }
        return bitmap;
    }
    return GetDefaultBitmapImage(new Uri("pack://application:,,,/Money;Component/Ribbon/Coins/blue.png"));
}

private object GetDefaultBitmapImage(Uri uri)
{
    // Здесь загрузка проходит корректно
    var bitmap = new BitmapImage();
    bitmap.BeginInit();
    bitmap.UriSource = uri;
    bitmap.EndInit();
    return bitmap;
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo
culture)
{
    if (value is BitmapSource && targetType != null && targetType.Equals(typeof(string)))
    {
        var bitmap = value as BitmapImage;
        if (bitmap != null && bitmap.StreamSource.CanRead)
        {
            byte[] buffer = new byte[bitmap.StreamSource.Length];
            var count = bitmap.StreamSource.Read(buffer, 0, buffer.Length);
            if (count == buffer.Length)
            {
                return System.Convert.ToBase64String(buffer, Base64FormattingOptions.InsertLineBreaks);
            }
        }
    }
    return string.Empty;
}


Описание класса к которому идет привязка

public class CoinViewModel : Vm
{
    private Guid _ID;
    public Guid ID
    {
        get => _ID;
        set => Set(ref _ID, value, nameof(ID));
    }

    private string _Front;
    public string Front
    {
        get => _Front;
        set => Set(ref _Front, value, nameof(Front));
    }

    private string _Back;
    public string Back
    {
        get => _Back;
        set => Set(ref _Back, value, nameof(Back));
    }

    public static explicit operator CoinDataView(CoinViewModel vm)
    {
        return new CoinDataView()
        {
            ID = vm.ID,
            Name = vm.Name,
            Count = vm.Count,
            SeriaID = vm.SeriaID,
            Back = vm.Back,
            Front = vm.Front
        };
    }
}


ну и собственно говоря, так происходит привязка




    


Ответы

Ответ 1



Ваша ошибка в том, что вы освобождаете поток с данными, который использует BitmapImage, этого не надо делать — уберите using и напишите просто: bitmap.StreamSource = new MemoryStream(buffer);

суббота, 11 января 2020 г.

Динамическая смена ItemsSource в TabControl (Коллекции разных типов)

#c_sharp #wpf #xaml #mvvm #binding


Есть задача, отображения коллекции данных отфильтрованных и разделённых на 5 вкладок. 

Например есть коллекция сериалов и в каждой вкладке либо запланированные к просмотру,
либо просмотренные, либо брошенные на середине.
Есть также коллекция комиксов, с точно такой же сортировкой по вкладкам. 
Приложение отображает либо список сериалов, либо список комиксов. 
У сериалов есть помимо прочего поле "Серии", у комиксов же вместо этого есть поля
"Главы" и "Тома". То есть классы для них различаются. 

Нужно, чтобы программа работала в одном из двух режимов (либо только для комиксов,
либо только для сериалов). Кнопки переключения есть в главном меню. 

Вопрос: можно ли подобное реализовать с помощью одного TabControl, при условии следования
паттернам MVVM?

Сейчас это реализовано следующим образом:

    
        
            
                
            
        
        
            
                
                    
                        
                            
                                
                                
                                
                                
                            
                        
                    
                
            
        
    
    
        
            
                
            
        
        
            
                
                    
                        
                            
                                
                                
                                
                                
                                
                            
                        
                    
                
            
        
    




То есть, я использую 2 TabControl'а и в зависимости от режима отображаю лишь один
из них. Не знаю, на сколько правильный такой подход, поэтому и спрашиваю совета у SO.
    


Ответы

Ответ 1



Можно обойтись с использованием ContentControl и заданием DataTemplate для каждого объекта(для сериалов и для комиксов). Создадим базовый класс представляющий собой родителя для комиксов и сериалов. public class BaseVM : INotifyPropertyChanged { //какие-то свойства //реализация INotifyPropertyChanged } Создадим производные классы: public class MangaVM : BaseVM { public ObservableCollection<тип_комикса> MangaList { get; set; } } public class AnimeVM : BaseVM { public ObservableCollection<тип_сериала> AnimeList { get; set; } } public List Entertainments { get; set; } //список который будет в меню выбора public BaseVM SelectedEntertainment { get; set; }//выбранный элемент в зависимости от которого //будет меняться шаблон представления теперь XAML: Элемент описывающий меню с выбором я сделал в качестве ListBox: Теперь сам ContentControl. ContentControl сам подберет нужный шаблон представления в зависимости от типа привязанного элемента свойству Content P.S. Ваша реализация со сменой контролов через Visibility имеет право жить. Так как, при смене Visibility не происходит повторно отрисовка контрола, в отличие от ContentControl при смене Content'a. Такое решение я применяю когда контрол сложный, содержит много дочерних элементов.

пятница, 10 января 2020 г.

Почему DataGrid такой медленный, или как придать приличный вид GridView?

#c_sharp #net #wpf #binding


Пишу проект на WPF. На одном из View необходимо выводить клиенту довольно большой
по объему список объектов в виде таблицы. Идеально для этого подошёл бы DataGrid. Я
не очень-то люблю "изобретать велосипеды", поэтому нашёл множество уже готовых решений
(те же контролы от Syncfusion). Я уж было очень сильно обрадовался, однако столкнулся
с серьёзной проблемой: привязывая коллекцию большого объёма (более сотни объектов)
к DataGrid, процесс рендеринга самого контрола сильно затягивается (и это при том,
что, унаследовавшись ObservableCollection, я добавил метод AddRange(), чтобы при добавлении
каждого объекта не вызывалось событие интерфейса INotifyPropertyChanged). Если добавлять
200-300 объектов в коллекцию за раз и привязать их к DataGrid, процесс затянется уж
очень надолго и приложение может вовсе зависнуть. Получение данных из базы и формирование
модельных объектов происходят во вторичном потоке. Думал, что проблема в количестве
объектов при их создании, однако, после использования профайлера, я понял, что все
тормоза начинаются именно после вызова AddRange() на привязанной коллекции. Возможно,
проблема в том, что каждый модельный объект реализует INPC? И содержит в себе ряд таких
же вложенных объектов, также реализующих этот интерфейс?
Как бы там ни было, я попробовал заменить DataGrid на GridView, и, в принципе, всё
стало работать довольно быстро. Однако GridView не предоставляет множества преимуществ,
которые имеются у DataGrid (начиная от элементарной сортировки и заканчивая готовым
красивым оформлением). Теперь меня интересует, как придать более или менее приличный
вид GridView. Например, как придать ему стиль "Metro"? 
Или же есть какая-то возможность, чтобы оптимизировать работу DataGrid? Ну не верится
мне, что на рендеринг сотни модельных объектов уходит так много времени и это так затормаживает UI. 

UPDATE:

ViewModel:

public class TaskEntitiesListViewModel : StoreUsingViewModel, IPageViewModel
    {
        #region fields
        private TaskEntityModel currentTask;        
        private List cacheTaskEntityModelList;
        #endregion
        #region ctors
        public TaskEntitiesListViewModel() : base()
        {
            this.cacheTaskEntityModelList = new List();
            this.TaskEntities = new ObservableCollectionExtended();
          
        }
        #endregion
        #region INPCProperties        
        public ObservableCollectionExtended TaskEntities { get; set; }
        #endregion
        #region eventSubscribers
        [EventSubscription("topic://Authorization/CorrectCredentialsEntered", typeof(OnUserInterface))]
        [EventSubscription("topic://Application/ViewUsual", typeof(OnUserInterface))]
       
        public async void InitTaskEntitiesListUsualAsync()
        {            
            if (TaskEntities.Count != 0) TaskEntities.Clear();

            var taskEntitiesListFromDAL = await Task.Factory.StartNew(() => Store.GetTaskEntityList(t
=> t.Owner, t => t.Creator, t => t.Base).ToList());

            //wrapping each TaskEntity to TaskEntityModel
            if (cacheTaskEntityModelList.Count != 0) cacheTaskEntityModelList.Clear();
            taskEntitiesListFromDAL.ForEach(t => cacheTaskEntityModelList.Add(new
TaskEntityModel(t)));

            TaskEntities.AddRange(cacheTaskEntityModelList.Where(t => t.IsOver ==
false).Take(100).ToList()); 
        }
        #endregion
        #region IPageViewModel
        public string Name { get { return "TaskEntitiesList"; } }
        #endregion
        #region BaseViewModel
        public override void OnDataChangedGlobalEvent()
        {
            InitTaskEntitiesListUsualAsync();
        }
        #endregion        
    }


Код коллекции:

public class ObservableCollectionExtended : ObservableCollection
{
    private bool _suppressNotification = false;
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (!_suppressNotification)
            base.OnCollectionChanged(e);
    }

    public void AddRange(IEnumerable list)
    {
        if (list == null)
            throw new ArgumentNullException("list");

        _suppressNotification = true;

        foreach (T item in list)
            Add(item);

        _suppressNotification = false;
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

    public void ClearRange()
    {
        _suppressNotification = true;

        ClearItems();

        _suppressNotification = false;
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }
}


Во вьюшке в качестве ItemsSource для DataGrid используется public ObservableCollectionExtended
TaskEntities { get; set; }

UPDATE 2

код View:


    
        
        
    
    
        
            
                
                
                
                
                
                
                
                
                
            
        
    



UPDATE 3
Model:

   public class TaskEntityModel : BaseModel
    {
        #region fields
        private string prop1;
        private string prop2;
        private string prop3;
        private string prop4;
        private string prop5;
        private DateTime dateProp1;
        private DateTime dateProp2;
        private DateTime dateProp3;
        private DateTime dateProp4;     
        #endregion
        #region ctors 
        public TaskEntityModel(TaskEntity taskEntity) : base(taskEntity)
        {
            //filling fields            
        }        
        #endregion
        #region INPCProperties       

        public string Prop1
        {
            get { return prop1; }
            set
            {
                if (value != prop1)
                {
                    prop1 = value;
                    OnPropertyChanged("Prop1");
                }
            }
        }

        public DateTime DateProp1
        {
            get
            {
                if (dateProp1 == null)
                    dateProp1 = DateTime.Now;
                return dateProp1;
            }
            set
            {
                if (value != dateProp1)
                {
                    dateProp1 = value;
                    OnPropertyChanged("DateProp1");
                    OnPropertyChanged("DateProp2");
                }
            }
        }
        //and so on

        #endregion        
    }

    public abstract class BaseModel : ObservableObject 
    {
        #region fields
        protected int ID;
        #endregion
        public int UID
        {
            get { return ID; }
            set
            {
                if(value != ID)
                {
                    ID = value;
                    OnPropertyChanged("UID");
                }
            }
        }
        public BaseModel() { }
        public BaseModel(BaseEntity baseEntity)
        {
            if(baseEntity != null)
            { 
            this.ID = baseEntity.ID;
            }
        }
    }

    


Ответы

Ответ 1



Если у вас первая привязка проходит с нормальной скоростью, а добавление элементов вызывает тормоза, то вы неправильно работаете с коллекцией. Большие изменения должны происходить пакетно, а единственное уведомление должно быть new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset). Как вариант, если не писать свою коллекцию, то можно менять экземпляр коллекции с соответствующим уведомлением. А вот реализация INotifyPropertyChanged на самой коллекции интересует ItemsControl куда меньше. По коду коллекции: у вас ошибка в коде AddRange. Внутри метода вы вызываете публичный метод Add, которому глубоко наплевать на ваш флаг _suppressNotification и который рассылает всю пачку уведомлений каждый раз. Вам надо вызывать защищённый метод InsertItem. Или можете опуститься на уровень ниже и работать напрямую со свойством Items, но тогда на вашей совести будут вызовы OnPropertyChanged для Count и Item[], а также вызов CheckReentrancy. Хм. Нет. Не должно быть наплевать. Впрочем, вы перекрывате только OnCollectionChanged. Возможно, OnPropertyChanged тоже влияет.

понедельник, 6 января 2020 г.

TextBlock с подсветкой текста

#c_sharp #wpf #binding


Столкнулся с необходимостью выделения фрагмента текста в TextBlock, а именно определённых
ключевых слов по которым были отфильтрованы элементы ListBox , этот текстовый блок
собственно и содержащие

XAML, мой ваиант


                        
                            
                                 
                                    
                                        
                                        
                                            
                                                
                                                    
                                                        
                                                    
                                                
                                            
                                        
                                    
                        

                        
                    


Вариант XAML, рекомендованный VladD, не создаёт коллекцию HighlightRulesCollection
(хотя казалось бы даже отрабатывает конвертер)

Используется компонент написанный нашим соотечественником с открытым исходным Компонент

Описание компонента

Закомментированный код это старый TexBlock без выделения

Новый компонент HighlightTextBlock прекрасно выделяет текст если использовать статический
ресурс, как в примере, но когда я пытаюсь его привязать к текущему тексту он не может
найти это поле :( , я новенький в WPF помогите разобраться с 

HightlightedText="{Binding Path=title, Converter={StaticResource getFilter}}"


Изначально вопрос звучал как верно привязать это свойство к title ?
Мне дали совет по реструктуризации XAML, через ресурсы , но это не работает т.к.
не создаётся HighlightRulesCollection и выделение не работает.

В ходе обсуждения было высказано предположение что следует несколько модифицировать
сам компонент и поместить коллекцию HighlighRule (также) в визуальное дерево. Тогда
будет автоматически наследоваться DataContext и по идее будет работать привязка через
ElementName.

Подскажите как это можно реализовать? или может кто то внесёт правки в исходный код
компонента ?

Альтернативный вариант , подскажите, как реализовать такого рода функционал другими
методами ?

структура DataContext 

public ObservableCollection Procedures { set; get; }
public CollectionViewSource ProceduresView { set; get; } = new CollectionViewSource();

    ....

    Procedures = new ObservableCollection();

    ProceduresView.Filter += Procedures_Filter;
    ProceduresView.Source = Procedures;

    ....

public class Procedure : ObservableObject 
{
    ....
    public String title { get; set; }
    ....
}
....

// Просто фильтрация

void Procedures_Filter(object sender, FilterEventArgs e)
{
    Procedure procedure = (Procedure) e.Item;
    Boolean flag = false;
    if (!string.IsNullOrEmpty(filter))
    {
        Setting.Filter sfilter = new Setting.Filter();
        sfilter.type = "искать везде";
        sfilter.text = filter;
        ObservableCollection arr = new ObservableCollection();
        arr.Add(sfilter);
        if (Utils.AssignedProcedureFromFilter(procedure, arr)) flag = true;
    }
    else flag = true;
    e.Accepted = flag;
}


Видео с описанием проблемы 

Упрощённый проект эмитирующий мой функционал
    


Ответы

Ответ 1



Если вы пользуетесь MVVM, то ваш title должен быть открытым свойством в вашей VM. То есть, в DataContext'е. Если вы привязываетесь не к DataContext'у, то вы должны указать Source (если вы привязываетесь к какому-то другому объекту) или ElementName (если к свойству другого контрола). Обновление: Ваш случай, на самом деле, более серьёзный. DataContext у вас, судя по всему, правильный. Но ваше Binding-выражение находится внутри сеттера свойства HighlightRules, которое не является частью логического дерева (т. к. не доступно как Child-элемент вашего контрола). А элементы, которые не находятся внутри логического дерева, принимают участие в привязках лишь ограничено: в них нет ни наследования DataContext'а, ни доступа по имени через ElementName. В качестве решения должна сработать привязка к элементу через x:Reference. В моём (сильно порезанном) тестовом примере срабатывает HightlightedText="{Binding Path=DataContext.title, Source={x:Reference MainText}}". Но, если прямо заменить на это, срабатывает странная ошибка: 'Не удается вызвать MarkupExtension.ProvideValue из-за циклической зависимости. Свойства внутри MarkupExtension не могут ссылаться на объекты, ссылающиеся на результат MarkupExtension. Метод обхода ошибки нашёлся здесь: нужно положить ваш элемент в ресурсы. Получаем вот такое: Обновление. По идее, можно решить проблему более простым образом: поместить все VisualRule в логическое дерево. Это делается так: в классе HighlightTextBlock добавляем метод protected override IEnumerator LogicalChildren { get { var baseChildrenIter = base.LogicalChildren; while (baseChildrenIter.MoveNext()) yield return baseChildrenIter.Current; var rules = HighlightRules; if (rules != null) { foreach (var rule in rules) yield return rule; } } } Далее, в методе HighlightRulesChanged класса HighlightTextBlock дописываем: foreach (var rule in col) { rule.HighlightTextChanged += tb.Rule_HighlightTextChanged; // добавляем это: tb.AddLogicalChild(rule); } А также в соседнем HighlightRules_CollectionChanged добавляем case System.Collections.Specialized.NotifyCollectionChangedAction.Add: foreach (HighlightRule rule in e.NewItems) { // добавляем это: AddLogicalChild(rule); _ruleTasks.Add(rule, new TaskQueue(1)); SubscribeRuleNotifies(rule); BeginHighlight(rule); } break; case System.Collections.Specialized.NotifyCollectionChangedAction.Remove: foreach (HighlightRule rule in e.OldItems) { // добавляем это: RemoveLogicalChild(rule); //rule.HightlightedText = string.Empty; _ruleTasks.Remove(rule); UnsubscribeRuleNotifies(rule); } break; case System.Collections.Specialized.NotifyCollectionChangedAction.Reset: foreach (HighlightRule rule in e.OldItems) { // добавляем это: RemoveLogicalChild(rule); //rule.HightlightedText = string.Empty; _ruleTasks.Remove(rule); UnsubscribeRuleNotifies(rule); } break; После этого можно будет привязываться так:

Ответ 2



Нашёл работающее решение своей задачи public class SearchHightlightTextBlock : TextBlock { public SearchHightlightTextBlock() : base() { } public String SearchText { get { return (String)GetValue(SearchTextProperty); } set { SetValue(SearchTextProperty, value); } } private static void OnDataChanged(DependencyObject source, DependencyPropertyChangedEventArgs e) { TextBlock tb = (TextBlock)source; if (tb.Text.Length == 0) return; string textUpper = tb.Text.ToUpper(); String toFind = ((String)e.NewValue).ToUpper(); int firstIndex = textUpper.IndexOf(toFind); String firstStr = ""; String foundStr = ""; if (firstIndex != -1) { firstStr = tb.Text.Substring(0, firstIndex); foundStr = tb.Text.Substring(firstIndex, toFind.Length); } String endStr = tb.Text.Substring(firstIndex + toFind.Length, tb.Text.Length - (firstIndex + toFind.Length)); tb.Inlines.Clear(); tb.FontSize = 16; var run = new Run(); run.Text = firstStr; tb.Inlines.Add(run); run = new Run(); run.Background = Brushes.Yellow; run.Text = foundStr; tb.Inlines.Add(run); run = new Run(); run.Text = endStr; tb.Inlines.Add(run); } public static readonly DependencyProperty SearchTextProperty = DependencyProperty.Register("SearchText", typeof(String), typeof(SearchHightlightTextBlock), new FrameworkPropertyMetadata(null, OnDataChanged)); } Использовать вот так Не совсем элегантное решение , но работает привязка и все дела :)

четверг, 2 января 2020 г.

binding коллекции к listbox в xaml

#c_sharp #wpf #binding


У меня есть коллекция public ObservableCollection MyCollection
у класса MyClass есть интовое свойство.

Так же есть листбокс, который я хочу заполнить объектами этой коллекции, при нажатии
на каждый элемент в текстбоксе должно высвечиваться свойство выбранного элемента, но
писать listBox1.DataSource/ItemsSource = ... - нельзя, надо делать биндинг только в
xaml'e и я никак не разберусь, как это реализовать. коллекция заполнена и инициализирована
    


Ответы

Ответ 1



Нужно завести свойство выбранного элемента в коллекции (Не забудьте о реализации INotifyPropertyChanged): public MyClass SelectedItem {get; set; } Далее в xaml делаете привязку к этому свойству: UPDATE Приведу код целиком: // модель представления элемента коллекции public sealed class CollectionItemVm : BaseVm { public int IntProperty { get { return _intProperty; } set { SetProperty(ref _intProperty, value); } } private int _intProperty; } // модель представления главного окна public sealed class MainVm : BaseVm { public ObservableCollection MyCollection { get { return _myCollection ?? (_myCollection = new ObservableCollection()); } } private ObservableCollection _myCollection; public CollectionItemVm SelectedItem { get { return _selectedItem; } set { SetProperty(ref _selectedItem, value); } } private CollectionItemVm _selectedItem; public MainVm() { MyCollection.Add(new CollectionItemVm { IntProperty = 1 }); MyCollection.Add(new CollectionItemVm { IntProperty = 2 }); SelectedItem = MyCollection.Last(); } } // представление главного окна BaseVm реализация INotifyPropertyChanged. WpfApplication4.ViewModels - пространство имен, в котором расположен класс MainVm.

Ответ 2



Всё просто. Трюк в том, что у коллекции* есть понятие текущего элемента. Для того, чтобы при выделении этот текущий элемент обновлялся, нужно IsSynchronizedWithCurrentItem="True". Вы можете в Binding'е сослаться на текущий элемент используя /. Length — то самое интовое свойство. *На самом деле, не у коллекции, а у CollectionView, который неявно «надевается» на коллекцию, когда к ней происходит Binding.

вторник, 31 декабря 2019 г.

WPF MVVM: поиск и scroll в listview

#c_sharp #wpf #binding


Есть listview. Он забайдин с ObservableCollection. Возможно ли организовать
поиск "на лету" по коллекции с выделением найденного? Т.е. есть TextBox с триггером
EventName="TextChanged" и при вводе туда символов, в listview отображать подходящие
записи. Нашел такой вариант, но мне нужен MVVM. 
    


Ответы

Ответ 1



Задача решается легко, кроме Binding ничего не требуется. Разметка: ViewModel: class MainVM : INotifyPropertyChanged { string _pattern; public string Pattern { get => _pattern; set { Set(ref _pattern, value); Selected = Strings.FirstOrDefault(s => s.StartsWith(Pattern)); } } string _selected; public string Selected { get => _selected; set => Set(ref _selected, value); } public ObservableCollection Strings { get; } public MainVM() { Strings = new ObservableCollection { "Телевизор", "Телефон", "Кровать", "Чемодан", "Стол", "Шкаф", "Чайник" }; } protected void Set(ref T field, T value, [CallerMemberName] string propertyName = "") { field = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } public event PropertyChangedEventHandler PropertyChanged; } Добавим скролл до выделенного Item. Подпишемся на событие в разметке: Обработчик события в MainWindow.xaml.cs: private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) { var listBox = sender as ListBox; listBox.ScrollIntoView(listBox.SelectedItem); }

пятница, 27 декабря 2019 г.

Не отображаются контролы, ссыляющиеся на шаблоны из MergedDictionaries (WPF)

#wpf #xaml #binding #resources


Имеется файл разметки для шаблона кнопки:


    
    
        
            
                                
                
            
        
        
            
            
                
            
            
            
                
                
            
        
    



Этот файл декларирован в App.xaml:


    

        
                            
                
                
            
        
    



В окне для кнопки прописываю следующий код:


Ответы

Ответ 1



Я всегда использовал StaticResource, и никогда никаких проблем не возникало. Попробуйте.

Как привязать контролы к иерархическим данным?

#c_sharp #winforms #binding


Возможно ли в WinForms привязать контролы к иерархическим данным?
Например, есть следующий xml:


    
        
            
        
    
    
        
            
        
    



Надо все значения level1 name вывести в один DataGridView. Все level2 name вывести
в другой DataGridView. И все level3 name - в третий.
Значения должны выводиться с учетом иерархии. Например, если в DataGridView для level1
текущей строкой является l11, то в DataGridView для level2 выводится l12.
Значения в каждом из DataGridView можно добавлять/удалять/редактировать и при этом
должен сразу выводиться измененный xml. 


    


Ответы

Ответ 1



В WinForms у контролов есть свойства DataSource и DataMember, которые позволяют привязывать контрол к источнику данных. Для создания xml-редактора в WinForms, как показано на рис., достаточно следующего кода. using System; using System.Data; using System.Drawing; using System.Windows.Forms; using System.Linq; using System.IO; class Program { [STAThread] static void Main(string[] args) { var d = Model.Create(); var l1 = new DataGridView() { Bounds = new Rectangle(0, 0, 249, 400), BorderStyle = BorderStyle.None, ColumnHeadersVisible = false, DataSource = d, DataMember = "level1" }; var l2 = new DataGridView() { Bounds = new Rectangle(250, 0, 249, 199), BorderStyle = BorderStyle.None, ColumnHeadersVisible = false, DataSource = d, DataMember = "level1.level1_level2" }; var l3 = new DataGridView() { Bounds = new Rectangle(250, 200, 249, 200), BorderStyle = BorderStyle.None, ColumnHeadersVisible = false, DataSource = d, DataMember = "level1.level1_level2.level2_level3" }; var t = new RichTextBox() { Bounds = new Rectangle(501, 0, 349, 400), BorderStyle = BorderStyle.None, ReadOnly = true, Text = Model.GetXml(d) }; var f = new Form() { ClientSize = new Size(850, 400), FormBorderStyle = FormBorderStyle.FixedDialog, MaximizeBox = false, MinimizeBox = false }; f.Controls.AddRange(new Control[] { l3, l2, l1, t }); foreach (var g in f.Controls.OfType()) g.BindingContext[g.DataSource, g.DataMember].CurrentItemChanged += (s, e) => t.Text = Model.GetXml(d); f.ShowDialog(); } class Model { public static object Create() { var xml = @" "; var ds = new DataSet(); ds.ReadXml(new StringReader(xml)); return ds; } public static string GetXml(object model) { var ds = model as DataSet; var sw = new StringWriter(); ds.WriteXml(sw); return sw.ToString(); } } }

четверг, 12 декабря 2019 г.

Зачем усложнять жизнь… Converter в Binding

#c_sharp #wpf #mvvm #binding


Здравствуйте, скажите пожалуйста зачем и в чём удобства Converter'а в Binding'е?
    


Ответы

Ответ 1



Конвертеры нужны, чтобы не выносить в VM логику представления. VM, например, не должна хотеть знать ширину колонки таблицы, текущий язык, выбранный скин приложения и тому подобные чисто вьюшные подробности. Вот для перевода VM-объектов во вьюшный вид и нужны конвертеры. Примеры. У вас есть VM-объект, означающий дату. Его тип, понятно, DateTime. А в UI вы хотите показывать его в виде «сегодня», «вчера», «неделю назад», «1 апреля» или как-то ещё. Перевод в такую нотацию — хорошее место для конвертера. Вам нужно вывести «15 экземпляров», но если колонка слишком узкая, то нужно сократить текст до «15 экз.». Заниматься размерами колонок — не дело VM, напишите для этого конвертер. В VM у вас язык сообщения, а во View вы решили показывать его в виде флажка нужной страны. Конвертация CultureInfo в pack URI к вашему графическому ресурсу — не задача VM, оставьте это конвертеру. VM вообще не знает ничего о графических ресурсах. В VM некоторые данные иногда отсутствуют (значение свойства равно null), и вы не хотите показывать рамку с красивым фоном для пустых данных. Превратить null в Visibility.Collapsed — конвертер и только он. У вас есть клетка игровой доски, и вам нужно выяснить её координаты. (Предположим, что ваши клетки не квадратные, и в грид автоматически не укладываются.) Размер клетки зависит от размера окна, количества клеток и ещё каких-то чисто визуальных подробностей. Кто будет пересчитывать координаты? VM? Фигушки, эту задачу должен решать конвертер. У вас есть кастомный прогрессбар, он не линейный, а круглый. Кто-то должен пересчитать положение прогрессбара в угол. VM? Нет, VM ничего не знает о стиле, который вы дали прогрессбару. А выставлять свойства для круглого, квадратного и семиугольного прогрессбаров совсем несемантично. Поэтому пусть стиль прогрессбара пользуется конвертером. Вам нужно привязать ширину одного контрола, чтобы она равнялась удвоенной ширине другого контрола. Кто будет умножать на два? Выносить задачу в VM через 18 привязок? Просто добавьте конвертер. В общем, для конвертеров найдётся много задач чисто визуального характера.

Ответ 2



Простой вариант - действительно делать все нужные свойства в VM и просто биндиться на них во View. Пока вас это устраивает, конвертер вам не нужен. С другой стороны, очень часто надо работать с чем-нибудь, для чего например есть стандартные входные данные и стандартное отображение. Как пример - Markdown разметка, у которой есть вполне себе стандарты написания, но выводить голым текстом такое неудобно, поэтому вполне пригодится конвертер из Markdown в FlowDocument. Другая ситуация, когда конвертер часто пригождается - изображения. Основной способ хранения и передачи бинарных данных - массив байт, увидеть его в VM - вполне нормальная ситуация. Соотвественно, для отображения потребуется конвертер, который например заточен на определенный формат изображений, размеры или ещё какие то настройки. Ещё всегда можно использовать конвертеры не совсем по назначению. Встречал такое редко, но тем не менее. Например, для ввода условного номера телефона по маске - хранить и передавать номер будет в виде цифр исключительно, а отображаться в каком нибудь типа +8(999)123-34-56.

суббота, 30 ноября 2019 г.

Drag&Drop по правилам WPF

#c_sharp #wpf #mvvm #binding #drag_n_drop


Подскажите пожалуйста как сделать правильно drag&drop прибиндив его положение к MVVM,
чтоб можно было бы его сохранить в файл просеарилизировав в JSON? Я знаю как это всё
делается по "старинке", поставить событие на Mouse_Down, Mouse_Move и Mouse_Up, а когда
происходит перетаскивание в Mouse_Move делать на подобии "canvas.setleft(uiobj, canvas.getleft(uiobj)+mouse.x)"
(этот пример не будет работать я знаю), а вот как в mvvm всё это переделать я не знаю :-(


    


Ответы

Ответ 1



Смотрите. Давайте поделим всё на визуальную и модельную части. Визуальная часть занимается перетаскиванием. При определении факта начала перетаскивания нужно отвязаться от VM (проще всего, наверное, скрыть элемент и показать на его месте другой, заодно можно немного поменять его вид, например, на полупрозрачный), обработать перетаскивание через MouseMove/MouseUp, определить, куда был дропнут элемент, отослать новые координаты в VM, и снова включить отображение элемента. VM обновит данные, новые координаты вступят в силу через привязку. Всё. Написал простой пример MVVM-приложения с перетаскиванием. Приложение рисует набор из перетаскиваемых квадратов. Программа простая, поэтому я обхожусь без модели. Будет выглядеть так: Начинаем с VM. Общий суперкласс, чтобы не имплементировать INPC каждый раз (если вы пользуетесь MVVM-фреймворком, у вас наверняка такой уже определён): class VM : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void NotifyPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } Теперь класс, представляющий собой фигуру. Мы выкладываем публичные свойства с текущим значением позиции, и командой для смены позиции. Если вы будете сериализировать этот объект, не забудьте отметить команду несериализируемой. class SquareVM : VM { public SquareVM() { RequestMove = new SimpleCommand(MoveTo); } // стандартное свойство Point position; public Point Position { get { return position; } set { if (position != value) { position = value; NotifyPropertyChanged(); } } } // выставляем команду, которая занимается перемещением public ICommand RequestMove { get; } void MoveTo(Point newPosition) { // в реальности тут могут быть всякие проверки, конечно Position = newPosition; } } Я использую примитивнейший вариант команды: class SimpleCommand : ICommand { readonly Action onExecute; public SimpleCommand(Action onExecute) { this.onExecute = onExecute; } public event EventHandler CanExecuteChanged; public bool CanExecute(object parameter) => true; public void Execute(object parameter) => onExecute((T)parameter); } Теперь, главная VM, ничего особенного: class MainVM : VM { public ObservableCollection Squares { get; } = new ObservableCollection() { new SquareVM() { Position = new Point( 30, 30) }, new SquareVM() { Position = new Point(100, 70) }, new SquareVM() { Position = new Point( 80, 0) }, new SquareVM() { Position = new Point( 90, 180) }, new SquareVM() { Position = new Point(200, 200) } }; } На этом с VM-частью покончено. Теперь, приложение. Стандартная заготовка для MVVM: App.xaml без StartupUri и переопределение OnStartup: public partial class App : Application { MainVM mainVM = new MainVM(); protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); new MainWindow() { DataContext = mainVM }.Show(); } } Переходим к интересной части: представление. Окно отображает список элементов в Canvas'е. Для привязки списка элементов использован, как обычно, ItemsControl: Сам контрол тоже несложен, код для обработки мышиных сообщений я честно стащил из ответа на вопрос «Как отследить перемещение одного окна над другим?» и выкинул всё ненужное. Маленькая тонкость: чтобы не следить за командой в DataContext'е, я объявил DependencyProperty RequestMoveCommand, и установил Binding на него. Также мы подписались в XAML'е на MouseDown и MouseUp. public partial class DraggableSquare : UserControl { public DraggableSquare() { InitializeComponent(); // устанавливаем Binding RequestMove из VM на свойство RequestMoveCommand: SetBinding(RequestMoveCommandProperty, new Binding("RequestMove")); } // стандартное DependencyProperty #region dp ICommand RequestMoveCommand public ICommand RequestMoveCommand { get { return (ICommand)GetValue(RequestMoveCommandProperty); } set { SetValue(RequestMoveCommandProperty, value); } } public static readonly DependencyProperty RequestMoveCommandProperty = DependencyProperty.Register("RequestMoveCommand", typeof(ICommand), typeof(DraggableSquare)); #endregion Vector relativeMousePos; // смещение мыши от левого верхнего угла квадрата Canvas container; // канвас-контейнер // по нажатию на левую клавишу начинаем следить за мышью void OnMouseDown(object sender, MouseButtonEventArgs e) { container = FindParent(); relativeMousePos = e.GetPosition(this) - new Point(); MouseMove += OnDragMove; LostMouseCapture += OnLostCapture; Mouse.Capture(this); } // клавиша отпущена - завершаем процесс void OnMouseUp(object sender, MouseButtonEventArgs e) { FinishDrag(sender, e); Mouse.Capture(null); } // потеряли фокус (например, юзер переключился в другое окно) - завершаем тоже void OnLostCapture(object sender, MouseEventArgs e) { FinishDrag(sender, e); } void OnDragMove(object sender, MouseEventArgs e) { UpdatePosition(e); } void FinishDrag(object sender, MouseEventArgs e) { MouseMove -= OnDragMove; LostMouseCapture -= OnLostCapture; UpdatePosition(e); } // требуем у VM обновить позицию через команду void UpdatePosition(MouseEventArgs e) { var point = e.GetPosition(container); // не забываем проверку на null RequestMoveCommand?.Execute(point - relativeMousePos); } // это вспомогательная функция, ей место в общей библиотеке private T FindParent() where T : FrameworkElement { FrameworkElement current = this; T t; do { t = current as T; current = (FrameworkElement)VisualTreeHelper.GetParent(current); } while (t == null && current != null); return t; } } Теперь совсем всё! Вот тут модификация этого кода, чтобы вместо квадратов таскался их уменьшенный и полупрозрачный предпросмотр.

Ответ 2



Как сделать это без "многокода" с использованием Catel (https://github.com/Catel/Catel), добавляем этот namespace: xmlns:catel="http://schemas.catelproject.com" в DragDrop.DataTemplate вы можете определить любой шаблон элемента который будет 'перетягиватся': ну а если Elements == ObservableCollection<> (более точно INotifyCollectionChanged), то подписываемся на ее событие во VM...и делаем все что нужно для обработки события изменения коллекции в результате DnD. P.S. не считаю вопрос Перемещение контролов по ItemsControl с использованием MVVM дублирующим, потому что данный вопрос более общий, а тот более прикладной... (никто не будет в рабочем проекте изобретать велосипед) так же смотрите ответ здесь: https://ru.stackoverflow.com/a/1045363/358860

вторник, 16 июля 2019 г.

Выполнение команд во вложенных панелях

Есть ItemsControl, к которому забиндили ObservableCollection обьектов.
Кнопки, которые появились в этом ItemsControl, не выполняют прибинженные к ним комманды. Проблем с командами нет, так как, если просто прибиндить команду к кнопке то команда выполняется. А в ItemsControl уже нет