Архив рубрики: WPF

Привязка к ресурсам в зависимости от данных

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

return Application.Current.FindResource(resourceName) as BitmapImage;

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


В рамках примера, я буду использовать простой класс с двумя свойствами:
public class Model
{
    public string Title { getset; }

    public string ResourceName { getset; }
}
Вот разметка формы:
<Window x:Class="WpfApplication4.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication4"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <BitmapImage x:Key="plus" UriSource="plus.png" />
        <BitmapImage x:Key="delete" UriSource="delete.png" />
        <local:NameToStyleConverter x:Key="NameToStyleConverter" />
    </Window.Resources>
    <Grid>
        <ItemsControl x:Name="icDemo">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <Image Style="{Binding ResourceName,Converter={StaticResource NameToStyleConverter}}" />
                        <TextBlock Text="{Binding Title}" />
                    </StackPanel>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</Window>
А вот заполнение данными списка при загрузке:
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        Loaded += MainWindow_Loaded;
    }

    private void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        List<Model> items = new List<Model>();
        items.Add(new Model() { Title = "Один", ResourceName = "plus" });
        items.Add(new Model() { Title = "Два", ResourceName = "plus" });
        items.Add(new Model() { Title = "Три", ResourceName = "delete" });
        items.Add(new Model() { Title = "Четыре", ResourceName = "plus" });
        items.Add(new Model() { Title = "Пять", ResourceName = "delete" });
        icDemo.ItemsSource = items;
    }
}
Как можно видеть из разметки, у контрола Image свойство Source не присваивается, а присваивается... Стиль! Ну а дальше все просто, получая имя ресурсы, мы конвертором создаем динамически стиль, в котором свойству Source присваиваем DynamicResource. Вот так:
public class NameToStyleConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        Style result = null;
        var name = value?.ToString();
        if (!string.IsNullOrWhiteSpace(name))
        {
            result = new Style(typeof(Image));

            var dynamicResource = new DynamicResourceExtension(name);

            var setter = new Setter()
            {
                Property = Image.SourceProperty,
                Value = dynamicResource
            };
            result.Setters.Add(setter);

        }
        return result;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
Ну и вот так это выглядит:
Как я уже сказал, таким образом надо поступать, если в конверторе нельзя просто извлечь ресурсы из Application. Так, кстати, можно и другие свойства задавать, необязательно привязку к ресурсам.

Привязка к ресурсам в зависимости от данных

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

return Application.Current.FindResource(resourceName) as BitmapImage;

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


В рамках примера, я буду использовать простой класс с двумя свойствами:
public class Model
{
    public string Title { getset; }

    public string ResourceName { getset; }
}
Вот разметка формы:
<Window x:Class="WpfApplication4.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication4"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <BitmapImage x:Key="plus" UriSource="plus.png" />
        <BitmapImage x:Key="delete" UriSource="delete.png" />
        <local:NameToStyleConverter x:Key="NameToStyleConverter" />
    </Window.Resources>
    <Grid>
        <ItemsControl x:Name="icDemo">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <Image Style="{Binding ResourceName,Converter={StaticResource NameToStyleConverter}}" />
                        <TextBlock Text="{Binding Title}" />
                    </StackPanel>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</Window>
А вот заполнение данными списка при загрузке:
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        Loaded += MainWindow_Loaded;
    }

    private void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        List<Model> items = new List<Model>();
        items.Add(new Model() { Title = "Один", ResourceName = "plus" });
        items.Add(new Model() { Title = "Два", ResourceName = "plus" });
        items.Add(new Model() { Title = "Три", ResourceName = "delete" });
        items.Add(new Model() { Title = "Четыре", ResourceName = "plus" });
        items.Add(new Model() { Title = "Пять", ResourceName = "delete" });
        icDemo.ItemsSource = items;
    }
}
Как можно видеть из разметки, у контрола Image свойство Source не присваивается, а присваивается... Стиль! Ну а дальше все просто, получая имя ресурсы, мы конвертором создаем динамически стиль, в котором свойству Source присваиваем DynamicResource. Вот так:
public class NameToStyleConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        Style result = null;
        var name = value?.ToString();
        if (!string.IsNullOrWhiteSpace(name))
        {
            result = new Style(typeof(Image));

            var dynamicResource = new DynamicResourceExtension(name);

            var setter = new Setter()
            {
                Property = Image.SourceProperty,
                Value = dynamicResource
            };
            result.Setters.Add(setter);

        }
        return result;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
Ну и вот так это выглядит:
Как я уже сказал, таким образом надо поступать, если в конверторе нельзя просто извлечь ресурсы из Application. Так, кстати, можно и другие свойства задавать, необязательно привязку к ресурсам.

Замена стандартной подсветки в TreeView

Пятница, голова уже не варит, максимум на что ее еще хватит написать ответ на очередной вопрос на форумах MSDN. Суть вопроса: нужно при выборе элемента в дереве не подсвечивать стандартное выделение всего TreeViewItem, а подсвечивать часть DataTemplate, который отвечает за показ элементов дерева. Т.е. сделать надо, чтобы выделение работало вот так, как на январе:

Интересно? Тогда идем под кат.



Первое с чего начнем, это с модели данных. Я особо усердствовать не буду и воспользуюсь, благо всего неделя до нового года, вот таким классом:
public class Model : DependencyObject
{
    public bool IsSelected
    {
        get { return (bool)GetValue(IsSelectedProperty); }
        set { SetValue(IsSelectedProperty, value); }
    }

    // Using a DependencyProperty as the backing store for IsSelected.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty IsSelectedProperty =
        DependencyProperty.Register("IsSelected"typeof(bool), typeof(Model), new PropertyMetadata(false));


    public string Name { getset; }

    public Model[] Children { getset; }

    public static Model[] DemoData
    {
        get
        {
            var result = new Model[1];
            result[0] = new Model() { Name = "Зима" };
            result[0].Children = new[] { new Model() { Name = "Декабрь" }, new Model() { Name = "Январь" }, new Model() { Name = "Февраль" } };
            return result;
        }
    }
}
Теперь переходим к разметке.
Добавляем в дерево загрузку данных:
<TreeView ItemsSource="{Binding Source={x:Static local:Model.DemoData}}">
Теперь убираем подсветку выбранной ячейки. Для этого меняем цвет фона выбранного элемента и цвет текста выбранного элемента:
<TreeView.Resources>
    <Style TargetType="TreeViewItem">
        <Style.Resources>
            <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Transparent"/>
            <SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="Black"/>
        </Style.Resources>
        <Setter Property="IsSelected" Value="{Binding IsSelected}" />
    </Style>
</TreeView.Resources>
Теперь, чтобы не заморачиваться с DataTriger-ами добавим простенький конвертер:
public class IsSelectedToBrushConverter : IValueConverter
{
    static Brush HighlightBrush = new SolidColorBrush(Colors.LightBlue);

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        Brush result = null;
        if (value is bool && (bool)value)
        {
            result = HighlightBrush;
        }
        return result;
    }

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

Подключаем этот конвертер в ресурсы окна:
<Window.Resources>
    <local:IsSelectedToBrushConverter x:Key="IsSelectedToBrushConverter" />
</Window.Resources>
Все, осталось сделать HierarchicalDataTemplate, в котором будет подсвечивать только то, что нам нужно:
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
    <StackPanel Orientation="Horizontal">
        <Image Source="Images/snowflake.PNG" Width="20" />
        <TextBlock Text="{Binding Name}" Background="{Binding IsSelected,Converter={StaticResource IsSelectedToBrushConverter}}" />
        <Image Source="Images/snowflake.PNG" Width="20" />
    </StackPanel>
</HierarchicalDataTemplate>
Как все это выглядит, можно посмотреть в картинке из заголовка поста.

Замена стандартной подсветки в TreeView

Пятница, голова уже не варит, максимум на что ее еще хватит написать ответ на очередной вопрос на форумах MSDN. Суть вопроса: нужно при выборе элемента в дереве не подсвечивать стандартное выделение всего TreeViewItem, а подсвечивать часть DataTemplate, который отвечает за показ элементов дерева. Т.е. сделать надо, чтобы выделение работало вот так, как на январе:

Интересно? Тогда идем под кат.



Первое с чего начнем, это с модели данных. Я особо усердствовать не буду и воспользуюсь, благо всего неделя до нового года, вот таким классом:
public class Model : DependencyObject
{
    public bool IsSelected
    {
        get { return (bool)GetValue(IsSelectedProperty); }
        set { SetValue(IsSelectedProperty, value); }
    }

    // Using a DependencyProperty as the backing store for IsSelected.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty IsSelectedProperty =
        DependencyProperty.Register("IsSelected"typeof(bool), typeof(Model), new PropertyMetadata(false));


    public string Name { getset; }

    public Model[] Children { getset; }

    public static Model[] DemoData
    {
        get
        {
            var result = new Model[1];
            result[0] = new Model() { Name = "Зима" };
            result[0].Children = new[] { new Model() { Name = "Декабрь" }, new Model() { Name = "Январь" }, new Model() { Name = "Февраль" } };
            return result;
        }
    }
}
Теперь переходим к разметке.
Добавляем в дерево загрузку данных:
<TreeView ItemsSource="{Binding Source={x:Static local:Model.DemoData}}">
Теперь убираем подсветку выбранной ячейки. Для этого меняем цвет фона выбранного элемента и цвет текста выбранного элемента:
<TreeView.Resources>
    <Style TargetType="TreeViewItem">
        <Style.Resources>
            <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Transparent"/>
            <SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="Black"/>
        </Style.Resources>
        <Setter Property="IsSelected" Value="{Binding IsSelected}" />
    </Style>
</TreeView.Resources>
Теперь, чтобы не заморачиваться с DataTriger-ами добавим простенький конвертер:
public class IsSelectedToBrushConverter : IValueConverter
{
    static Brush HighlightBrush = new SolidColorBrush(Colors.LightBlue);

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        Brush result = null;
        if (value is bool && (bool)value)
        {
            result = HighlightBrush;
        }
        return result;
    }

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

Подключаем этот конвертер в ресурсы окна:
<Window.Resources>
    <local:IsSelectedToBrushConverter x:Key="IsSelectedToBrushConverter" />
</Window.Resources>
Все, осталось сделать HierarchicalDataTemplate, в котором будет подсвечивать только то, что нам нужно:
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
    <StackPanel Orientation="Horizontal">
        <Image Source="Images/snowflake.PNG" Width="20" />
        <TextBlock Text="{Binding Name}" Background="{Binding IsSelected,Converter={StaticResource IsSelectedToBrushConverter}}" />
        <Image Source="Images/snowflake.PNG" Width="20" />
    </StackPanel>
</HierarchicalDataTemplate>
Как все это выглядит, можно посмотреть в картинке из заголовка поста.

Показ дочерних View в рамках патерна MVVM (часть 2)

Пару лет назад уже была статья "Показ дочерних View в рамках патерна MVVM", т.к. сейчас это делаем по другому, да и вопрос тут возник на тостере... Еще раз, в рамках паттерна предполагается что ViewModel (бизнес-логика) работает только с классами ViewModel и Model, а нам необходимо показать окно, т.е. кроме создания ViewModel для него, нужно создать еще и View. Как это сделать? Четвертый вариант под катом.

Пример будет максимально упрощен, но основные идеи постараюсь показать.
1. Создаем пустой WPF проект. В него добавляем класс окно вот с такой разметкой:

<Window x:Class="ChildWindowsDemo.ChildWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ChildWindowsDemo"
        mc:Ignorable="d"
        Title="{Binding Title}"SizeToContent="WidthAndHeight">
    <Grid>
        <ContentPresenter Content="{Binding }" />
    </Grid>
</Window>

В коде у него ничего не добавляем. Именно в этом окне будут показываться все дочерние ViewModel. Его состояние можно привязать к модели, например, здесь показано как привязать заголовок, но точно так же можно Visability или другие свойства (для свойств типа Visability можно через конвертор, а в можели хранить bool).
2. Добавляем класс базового ViewModel:

public class ViewModelBase : DependencyObject
{
    ///
    ///Окно в котором показывается текущий ViewModel
    ///
    private ChildWindow _wnd = null;

    ///
    /// Заголовок окна
    ///
    public string Title
    {
        get { return (string)GetValue(TitleProperty); }
        set { SetValue(TitleProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Title.  This enables animation, styling, binding, etc...
    public static readonly DependencyPropertyTitleProperty =
        DependencyProperty.Register("Title", typeof(string), typeof(ViewModelBase), new PropertyMetadata(""));       

    ///
    /// Методы вызываемый окном при закрытии
    ///
    protected virtual void Closed()
    {

    }

    ///
    /// Методы вызываемый для закрытия окна связанного с ViewModel
    ///
    public bool Close()
    {
        var result = false;
        if (_wnd != null)
        {
            _wnd.Close();
            _wnd = null;
            result = true;               
        }
        return result;
    }

    ///
    /// Метод показа ViewModel в окне
    ///
    /// viewModel">
    protected void Show(ViewModelBase viewModel)
    {
        viewModel._wnd = new ChildWindow();
        viewModel._wnd.DataContext = viewModel;
        viewModel._wnd.Closed += (sender, e) => Closed();
        viewModel._wnd.Show();
    }
}
Потомки этого класса могут передавать произвольного потомка этого класса в метод Show, чтобы показать его в отдельном окне.
3. Создаем View для демо, т.к. у нас всегда показывается в окне из пункта 1, то View делаем на основе UserControl:

<UserControl x:Class="ChildWindowsDemo.View.DemoView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:ChildWindowsDemo.View"
             mc:Ignorable="d"
             d:DesignHeight="300" d:DesignWidth="300">
    <StackPanelWidth="200">
        <DatePicker SelectedDate="{Binding Date}" />
        <Button Command="{BindingCloseCommand}">Закрыть</Button>
    </StackPanel>
</UserControl>
Наш дочерний ViewModel позволяет вводить дату и содержит кнопку для закрытия окна.
4. Демонстрационный ViewModel, потомок нашего ViewModelBase:

class DemoViewModel : ViewModelBase
{
    public DateTime Date
    {
        get { return (DateTime)GetValue(DateProperty); }
        set { SetValue(DateProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Date.  This enables animation, styling, binding, etc...
    public static readonly DependencyPropertyDateProperty =
        DependencyProperty.Register("Date", typeof(DateTime), typeof(DemoViewModel), new PropertyMetadata(null));

    public ICommandCloseCommand
    {
        get { return (ICommand)GetValue(CloseCommandProperty); }
        set { SetValue(CloseCommandProperty, value); }
    }

    // Using a DependencyProperty as the backing store for CloseCommand.  This enables animation, styling, binding, etc...
    public static readonly DependencyPropertyCloseCommandProperty =
        DependencyProperty.Register("CloseCommand", typeof(ICommand), typeof(DemoViewModel), new PropertyMetadata(null));

    public DemoViewModel()
    {
        CloseCommand = new SimpleCommand(() => Close());
    }
}

5. Главный ViewModel тоже является потомком BaseViewModel, в нем реализуем показ дочернего окна вызовом метода Show:

class MainViewModel : ViewModelBase
{
    public ICommandCreateChildCommand { get; set; }

    public MainViewModel()
    {
        CreateChildCommand = new SimpleCommand(CreateChild);
    }

    private void CreateChild()
    {
        var child = new DemoViewModel()
        {
            Title = "Дочернее окно",
            Date = DateTime.Now
        };
        Show(child);
    }
}

6. Ну и магия, в ресурсах приложения создаем связку между View и ViewModel:
<Application.Resources>
    <DataTemplateDataType="{x:Type viewmodel:DemoViewModel}">
        <view:DemoViewHorizontalAlignment="Stretch" />
    </DataTemplate>
</Application.Resources>

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

Раскраска фона элементов ListBox через строчку

Как то всегда пользовался для отображения данных, в которых имеет смысл раскрашивать цвет фона через строчку DataGrid, а в нем все просто. Есть два замечательных свойства RowBackground и AlternatingRowBackground. Задаешь их и все работает. Если нужно на основе данных отображаемых в строках, то это тоже все просто, через Converter. А вот что делать, если через строчку надо раскрасить фон в ListBox?


Как оказалось, все достаточно просто.
У ListBox есть замечательное свойство AlternationCount которое позволяет задать периодичность изменения раскраски. Если нам нужно строка белая, потом серая, потом опять белая, то записываем в него 2. Если нам нужно чередовать больше цветов, то указываем их количество. Ну а дальше, задаем стиль, который через присоединенное свойство ItemsControl.AlternationIndex определяет каким по счету в рамках одного периода смены цвета является текущий ListBoxItem. Например, AlternationCount = 3. В этом случае у  у первого ListBoxItem свойство AlternationIndex = 0, у второго 1, у третьего 2, у четвертого опять 0 и т.д.
Ну и небольшая демка:
<ListBox AlternationCount="2">
    <ListBox.Resources>
        <Style TargetType="{x:TypeListBoxItem}">
            <Setter Property="Background"Value="Blue"/>
            <Setter Property="Foreground"Value="White"/>
            <Style.Triggers>
                <Trigger Property="ListBox.AlternationIndex"Value="1">
                    <Setter Property="Background"Value="White"/>
                    <Setter Property="Foreground"Value="Black"/>
                </Trigger>
                <Trigger Property="ListBox.AlternationIndex"Value="0">
                    <Setter Property="Background"Value="DarkBlue"/>
                    <Setter Property="Foreground"Value="White"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </ListBox.Resources>
    <ListBoxItem>1</ListBoxItem>
    <ListBoxItem>2</ListBoxItem>
    <ListBoxItem>3</ListBoxItem>
    <ListBoxItem>4</ListBoxItem>
    <ListBoxItem>5</ListBoxItem>
    <ListBoxItem>6</ListBoxItem>

</ListBox>
Вот так это выглядит:
На мой взгляд, все достаточно просто.

Раскраска фона элементов ListBox через строчку

Как то всегда пользовался для отображения данных, в которых имеет смысл раскрашивать цвет фона через строчку DataGrid, а в нем все просто. Есть два замечательных свойства RowBackground и AlternatingRowBackground. Задаешь их и все работает. Если нужно на основе данных отображаемых в строках, то это тоже все просто, через Converter. А вот что делать, если через строчку надо раскрасить фон в ListBox?


Как оказалось, все достаточно просто.
У ListBox есть замечательное свойство AlternationCount которое позволяет задать периодичность изменения раскраски. Если нам нужно строка белая, потом серая, потом опять белая, то записываем в него 2. Если нам нужно чередовать больше цветов, то указываем их количество. Ну а дальше, задаем стиль, который через присоединенное свойство ItemsControl.AlternationIndex определяет каким по счету в рамках одного периода смены цвета является текущий ListBoxItem. Например, AlternationCount = 3. В этом случае у  у первого ListBoxItem свойство AlternationIndex = 0, у второго 1, у третьего 2, у четвертого опять 0 и т.д.
Ну и небольшая демка:
<ListBox AlternationCount="2">
    <ListBox.Resources>
        <Style TargetType="{x:TypeListBoxItem}">
            <Setter Property="Background"Value="Blue"/>
            <Setter Property="Foreground"Value="White"/>
            <Style.Triggers>
                <Trigger Property="ListBox.AlternationIndex"Value="1">
                    <Setter Property="Background"Value="White"/>
                    <Setter Property="Foreground"Value="Black"/>
                </Trigger>
                <Trigger Property="ListBox.AlternationIndex"Value="0">
                    <Setter Property="Background"Value="DarkBlue"/>
                    <Setter Property="Foreground"Value="White"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </ListBox.Resources>
    <ListBoxItem>1</ListBoxItem>
    <ListBoxItem>2</ListBoxItem>
    <ListBoxItem>3</ListBoxItem>
    <ListBoxItem>4</ListBoxItem>
    <ListBoxItem>5</ListBoxItem>
    <ListBoxItem>6</ListBoxItem>

</ListBox>
Вот так это выглядит:
На мой взгляд, все достаточно просто.

Показ окна в отдельном потоке

Время от времени в приложении появляются "долгие операции" во время которых интерфейс тормозит и пользователь не понимает что происходит с приложением. Обычно такие операции выносятся в фоновый поток, а в основном потоке приложения показываем прогресс выполнения работы или некую анимацию дающую понять, что приложение не повисло. Но что делать, если работа выполняется в основном потоке и вынести ее в фоновый нельзя (например, идет чтение из визуальных компонентов)? Вот об этом и поговорим под катом.


Итак еще раз задача. В приложении есть "длительная операция", которая выполняется в потоке интерфейса и нужно пользователю показать анимацию, чтобы он не волновался что все пропало.
Для показа анимации я добавил вот такое окно в приложение:

<Window x:Class="WpfApplication1.BusyWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication1"
        mc:Ignorable="d"
        Title="BusyWindow" Height="300"Width="300" WindowStyle="None" Background="Transparent" AllowsTransparency="True">
    <Grid>
        <Grid Background="Transparent" HorizontalAlignment="Center" VerticalAlignment="Center">
            <CanvasRenderTransformOrigin="0.5,0.5" HorizontalAlignment="Center" VerticalAlignment="Center" Width="50" Height="50" >
                <Ellipse Width="10" Height="10" Canvas.Left="20" Canvas.Top="0" Stretch="Fill" Fill="Green" Opacity="1.0"/>
                <Ellipse Width="10" Height="10" Canvas.Left="40" Canvas.Top="20" Stretch="Fill" Fill="Green" Opacity="0.1"/>
                <Ellipse Width="10" Height="10" Canvas.Left="20" Canvas.Top="40" Stretch="Fill" Fill="Green" Opacity="0.4"/>
                <Ellipse Width="10" Height="10" Canvas.Left="0" Canvas.Top="20" Stretch="Fill" Fill="Green" Opacity="0.7"/>
                <Canvas.RenderTransform>
                    <RotateTransform Angle="0" />
                </Canvas.RenderTransform>
            </Canvas>
            <CanvasRenderTransformOrigin="0.5,0.5" HorizontalAlignment="Center" VerticalAlignment="Center" Width="50" Height="50" >
                <Ellipse Width="10" Height="10" Canvas.Left="20" Canvas.Top="0" Stretch="Fill" Fill="Green" Opacity="0.01"/>
                <Ellipse Width="10" Height="10" Canvas.Left="40" Canvas.Top="20" Stretch="Fill" Fill="Green" Opacity="0.2"/>
                <Ellipse Width="10" Height="10" Canvas.Left="20" Canvas.Top="40" Stretch="Fill" Fill="Green" Opacity="0.5"/>
                <Ellipse Width="10" Height="10" Canvas.Left="0" Canvas.Top="20" Stretch="Fill" Fill="Green" Opacity="0.8"/>
                <Canvas.RenderTransform>
                    <RotateTransform Angle="30" />
                </Canvas.RenderTransform>
            </Canvas>
            <CanvasRenderTransformOrigin="0.5,0.5" HorizontalAlignment="Center" VerticalAlignment="Center" Width="50" Height="50" >
                <Ellipse Width="10" Height="10" Canvas.Left="20" Canvas.Top="0" Stretch="Fill" Fill="Green" Opacity="0.05"/>
                <Ellipse Width="10" Height="10" Canvas.Left="40" Canvas.Top="20" Stretch="Fill" Fill="Green" Opacity="0.3"/>
                <Ellipse Width="10" Height="10" Canvas.Left="20" Canvas.Top="40" Stretch="Fill" Fill="Green" Opacity="0.6"/>
                <Ellipse Width="10" Height="10" Canvas.Left="0" Canvas.Top="20" Stretch="Fill" Fill="Green" Opacity="0.9"/>
                <Canvas.RenderTransform>
                    <RotateTransform Angle="60" />
                </Canvas.RenderTransform>
            </Canvas>
            <Grid.RenderTransform>
                <RotateTransform x:Name="SpinnerRotate" CenterX="25" CenterY="25" />
            </Grid.RenderTransform>
            <Grid.Triggers>
                <EventTrigger RoutedEvent="FrameworkElement.Loaded">
                    <BeginStoryboard>
                        <Storyboard x:Name="Animation">
                            <DoubleAnimationUsingKeyFrames Duration="0:0:12" RepeatBehavior="Forever" SpeedRatio="12" Storyboard.TargetName="SpinnerRotate" Storyboard.TargetProperty="(RotateTransform.Angle)">
                                <DiscreteDoubleKeyFrame KeyTime="00:00:00" Value="0" />
                                <DiscreteDoubleKeyFrame KeyTime="00:00:01" Value="30" />
                                <DiscreteDoubleKeyFrame KeyTime="00:00:02" Value="60" />
                                <DiscreteDoubleKeyFrame KeyTime="00:00:03" Value="90" />
                                <DiscreteDoubleKeyFrame KeyTime="00:00:04" Value="120" />
                                <DiscreteDoubleKeyFrame KeyTime="00:00:05" Value="150" />
                                <DiscreteDoubleKeyFrame KeyTime="00:00:06" Value="180" />
                                <DiscreteDoubleKeyFrame KeyTime="00:00:07" Value="210" />
                                <DiscreteDoubleKeyFrame KeyTime="00:00:08" Value="240" />
                                <DiscreteDoubleKeyFrame KeyTime="00:00:09" Value="270" />
                                <DiscreteDoubleKeyFrame KeyTime="00:00:10" Value="300" />
                                <DiscreteDoubleKeyFrame KeyTime="00:00:11" Value="330" />
                            </DoubleAnimationUsingKeyFrames>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </Grid.Triggers>
        </Grid>
    </Grid>
</Window>

Кода у этого окна нет, все на триггерах. Итак к пример. На главном окне я добавил кнопку, вот с таким магическим кодом:

private void button_Click(object sender, RoutedEventArgs e)
{
    Thread.Sleep(4000);
}

Это и есть эмуляция "длительной операции". Понятно, что после нажатия этой кнопки приложение "висит" четыре секунды и непонятно, что с ним происходит. Давайте покажем мотылятор. Решение в лоб:

private void button_Click(object sender, RoutedEventArgs e)
{
    ShowBusy();
    Thread.Sleep(4000);
    HideBusy();
}

BusyWindow _busyWindow = null;

private void ShowBusy()
{
    _busyWindow = new BusyWindow();
    _busyWindow.Left = this.Left + this.Width / 2;
    _busyWindow.Top = this.Top + this.Height / 2;
    _busyWindow.Show();
}

private void HideBusy()
{
    _busyWindow.Close();
}

Позволяет показать мотылятор, но т.к. основной поток остановлен, то и анимация не происходит. Иллюзия что приложению плохо сохраняется.
Попытка вынести создание и показ окна в отдельный поток ни к чему хорошему не приводит. Изменив код вот так:

private void ShowBusy()
{
    Task.Factory.StartNew(AnimationThreadStartingPoint);
}

private void AnimationThreadStartingPoint()
{
    _busyWindow = new BusyWindow();
    _busyWindow.Left = this.Left + this.Width / 2;
    _busyWindow.Top = this.Top + this.Height / 2;
    _busyWindow.Show();
}

При нажатии на кнопку мы получим вот такое печальное сообщение:
Ок, отказываемся от новомодных Task-ов и возвращаемся к привычным Thread-ам, избавляемся от межпотокового взаимодействия и добавляем блокировки в целях избегания гонок:

BusyWindow _busyWindow = null;

object _busyWindowSync = new object();

private void ShowBusy()
{
    lock(_busyWindowSync)
    {
        if (_busyWindow == null)
        {
            double left = Dispatcher.Invoke((Func<double>)(() => this.Left + this.Width / 2));
            double top = Dispatcher.Invoke((Func<double>)(() => this.Top + this.Height / 2));
            Thread newWindowThread = new Thread(new ParameterizedThreadStart(AnimationThreadStartingPoint));
            newWindowThread.SetApartmentState(ApartmentState.STA);
            newWindowThread.IsBackground = true;
            newWindowThread.Start(new Point() { X = left, Y = top });
        }
    }
}

private void AnimationThreadStartingPoint(object position)
{
    lock(_busyWindowSync)
    {
        if (_busyWindow == null)
        {
            _busyWindow = new BusyWindow();
            _busyWindow.Left = ((Point)position).X;
            _busyWindow.Top = ((Point)position).Y;
            _busyWindow.Show();
        }
    }
    System.Windows.Threading.Dispatcher.Run();
}

private void HideBusy()
{
    lock (_busyWindowSync)
    {
        if (_busyWindow != null)
        {
            _busyWindow.Dispatcher.BeginInvoke((Action)_busyWindow.Close);
        }
    }
}

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

Подписаться на изменение DependecyProperty

Часто бывает, что есть некий потомок DependencyObject, у него есть DependencyProperty, а вот события сообщающего о том, что свойство изменилось нет. Я с такой ситуацией столкнулся при попытках отследить изменение актуальной ширины DataGridColumn. Ну и под катом, как это все можно провернуть.

Для примера, я воспользуюсь вот таким классом:
class MyClass : DependencyObject
{
    public int MyProperty
    {
        get { return (int)GetValue(MyPropertyProperty); }
        set { SetValue(MyPropertyProperty, value); }
    }

    // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty MyPropertyProperty =
        DependencyProperty.Register("MyProperty"typeof(int), typeof(MyClass), new PropertyMetadata(0));
}
Ну и как мы можем узнать об изменении этого свойства? До очень просто, вот так:
static void Main(string[] args)
{
    MyClass m = new MyClass();
    m.MyProperty = 10; // До подписывания
    PropertyDescriptor descriptor = DependencyPropertyDescriptor.FromProperty(MyClass.MyPropertyProperty, typeof(MyClass));
    descriptor.AddValueChanged(m, new EventHandler((sender, e) => Console.WriteLine(((MyClass)sender).MyProperty)));
    m.MyProperty = 20;
    Console.ReadKey();
}
Ну и вот так это выглядит:
Первое изменение свойства прошло тихонечко, а на второе уже сработал вывод. 

Tooltip в виде выноски (Balloon)


Как оказалось делал зря, но прикольно, поэтому выложу, вдруг где еще пригодиться. Задача состоит в том, чтобы вывести ToolTip в виде выноски (балона-облачка).



Ларчик открывается достаточно просто. Заменяем ToolTip через стили на StackPanel, с Path для изображения треугольничка и Border для основного содержимого. Вот так:
<Style TargetType="ToolTip">
    <Setter Property="OverridesDefaultStyle" Value="true"/>
    <Setter Property="HasDropShadow" Value="True"/>
    <Setter Property="Placement" Value="Bottom" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ToolTip">
                <StackPanel>
                    <Path Margin="10,0,0,0" Fill="#e5323232" Data="M 0 6 L 6 0 L 12 6 Z"/>
                    <Border CornerRadius="3" HorizontalAlignment="Center" VerticalAlignment="Top" Padding="10,7" BorderThickness="0" Background="#e5323232">
                        <StackPanel>
                            <TextBlock FontFamily="Arial" FontSize="12" Text="{TemplateBinding Content}" Foreground="#f0f0f0" />
                        </StackPanel>
                    </Border>
                </StackPanel>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
Все, прикольный ToolTip получен.