πŸ“œ ⬆️ ⬇️

WPF Binding: The Power of Styles and Templates in WPF.

In WPF, there is a very clear separation between Control's behavior and how it looks. For example, the behavior of the object of the Button class is to respond to various events with a click, but it can be any kind - you can make a button in the form of an arrow, a fish, or something else that is suitable for your application. Redefining the Control display is very simple when using VS with styles and templates, and even easier if you have Microsoft Expression Blend. In this example, I will show you how to override the ListBox mapping that is used to display a list of planets.

I decided to start by creating a data source with planets and the sun. I defined the class β€œSolarSystemObject” with the following properties: Name, Orbit, Diameter, Image and Details. I overloaded the ToString () method in this class so that it returns the name of the object of the solar system. Then I added the class β€œSolarSystem” with the property β€œSolarSystemObjects” with type ObservableCollection. In the SolarSystem class constructor, I added the sun and nine planets to the SolarSystemObjects collection.

As soon as I identified the data source, I was ready to add to the main ListBox window that was associated with this collection:
< Window.Resources > < local:SolarSystem x:Key =” solarSystem ” /> (…) </ Window.Resources > < ListBox ItemsSource =”{ Binding Source ={ StaticResource solarSystem }, Path = SolarSystemObjects }” /> * This source code was highlighted with Source Code Highlighter .
  1. < Window.Resources > < local:SolarSystem x:Key =” solarSystem ” /> (…) </ Window.Resources > < ListBox ItemsSource =”{ Binding Source ={ StaticResource solarSystem }, Path = SolarSystemObjects }” /> * This source code was highlighted with Source Code Highlighter .
  2. < Window.Resources > < local:SolarSystem x:Key =” solarSystem ” /> (…) </ Window.Resources > < ListBox ItemsSource =”{ Binding Source ={ StaticResource solarSystem }, Path = SolarSystemObjects }” /> * This source code was highlighted with Source Code Highlighter .
  3. < Window.Resources > < local:SolarSystem x:Key =” solarSystem ” /> (…) </ Window.Resources > < ListBox ItemsSource =”{ Binding Source ={ StaticResource solarSystem }, Path = SolarSystemObjects }” /> * This source code was highlighted with Source Code Highlighter .
  4. < Window.Resources > < local:SolarSystem x:Key =” solarSystem ” /> (…) </ Window.Resources > < ListBox ItemsSource =”{ Binding Source ={ StaticResource solarSystem }, Path = SolarSystemObjects }” /> * This source code was highlighted with Source Code Highlighter .
  5. < Window.Resources > < local:SolarSystem x:Key =” solarSystem ” /> (…) </ Window.Resources > < ListBox ItemsSource =”{ Binding Source ={ StaticResource solarSystem }, Path = SolarSystemObjects }” /> * This source code was highlighted with Source Code Highlighter .
< Window.Resources > < local:SolarSystem x:Key =” solarSystem ” /> (…) </ Window.Resources > < ListBox ItemsSource =”{ Binding Source ={ StaticResource solarSystem }, Path = SolarSystemObjects }” /> * This source code was highlighted with Source Code Highlighter .

And so, the ListBox displays the planets, but visually it still looks somewhat rustic:


At this stage, I began to think about how to map the planets in the most realistic way - my goal was to achieve a display similar to the solar system diagrams in school books. The first step was to change the layout of ListBoxItem. The standard layout for ListBox is StackPanel, which causes ListBoxItem to appear one after another (to be more precise, this is VirtualizingStackPanel, which adds virtualization to the traditional StackPanel). In order to display the planets the way I want, I need a Canvas, which allows me to position elements in it according to a certain number of pixels to the left and on top of the borders of this Canvas. The ListBox has an ItemsPanel property of type ItemsPanelTemplate that can be used to change the layout of a ListBox, as is done in my example. This is how I did it:
  1. < Style TargetType = ” ListBox ” >
  2. < Setter Property = ” ItemsPanel ” >
  3. < Setter.Value >
  4. < ItemsPanelTemplate >
  5. < Canvas Width = ” 590 ? Height = ” 590 ? Background = " Black " />
  6. </ ItemsPanelTemplate >
  7. </ Setter.Value >
  8. </ Setter >
  9. </ Style >
* This source code was highlighted with Source Code Highlighter .

My next step was to define the mapping of each planet. I did this using a DataTemplate. I decided to represent each planet with its image and white ellipse, imitating its orbit around the sun. I also added a hint with detailed information about the planet, which appears when the cursor is over the planet.
  1. < DataTemplate DataType = "{x: Type local: SolarSObject}" >
  2. < Canvas Width = "20" Height = "20" >
  3. < Ellipse
  4. Canvas . Left = "{Binding Path = Orbit, Converter = {StaticResource convertOrbit}, ConverterParameter = -1.707}"
  5. Canvas . Top = "{Binding Path = Orbit, Converter = {StaticResource convertOrbit}, ConverterParameter = -0.293}"
  6. Width = "{Binding Path = Orbit, Converter = {StaticResource convertOrbit}, ConverterParameter = 2}"
  7. Height = "{Binding Path = Orbit, Converter = {StaticResource convertOrbit}, ConverterParameter = 2}"
  8. Stroke = "White"
  9. StrokeThickness = "1" />
  10. < Image Source = "{Binding Path = Image}" Width = "20" Height = "20" >
  11. < Image.ToolTip >
  12. < StackPanel Width = "250" TextBlock . FontSize = "12" >
  13. < TextBlock FontWeight = "Bold" Text = "{Binding Path = Name}" />
  14. < StackPanel Orientation = "Horizontal" >
  15. < TextBlock Text = "Orbit:" />
  16. < TextBlock Text = "{Binding Path = Orbit}" />
  17. < TextBlock Text = "AU" />
  18. </ StackPanel >
  19. < TextBlock Text = "{Binding Path = Details}" TextWrapping = "Wrap" />
  20. </ StackPanel >
  21. </ Image.ToolTip >
  22. </ Image >
  23. </ Canvas >
  24. </ DataTemplate >
  25. < Style TargetType = "ListBoxItem" >
  26. < Setter Property = "Canvas.Left" Value = "{Binding Path = Orbit, Converter = {StaticResource convertOrbit}, ConverterParameter = 0.707}" />
  27. < Setter Property = "Canvas. Bottom" Value = "{Binding Path = Orbit, Converter = {StaticResource convertOrbit}, ConverterParameter = 0.707}" />
  28. (...)
  29. </ Style >
* This source code was highlighted with Source Code Highlighter .

As you can see in the template and style above, the properties that determine the position of the ListBoxItem and the position and size of Ellips are based on the orbit of the planet and all use the same converter, only with different parameters. The task of the converter is to convert the distances between the objects of the solar system into the distances inside the Canvas in pixels. My first implementation of this converter simply multiplied the value of the orbit by a constant, but I found that the inner planets were very closely spaced to each other. Therefore, I decided to slightly change the calculation to make it nonlinear. I also decided that the converter would take a certain parameter that would scale the final result by some value so that I could use this logic many times.
  1. public class ConvertOrbit: IValueConverter
  2. {
  3. public object Convert ( object value , Type targetType, object parameter, CultureInfo culture)
  4. {
  5. double orbit = ( double ) value ;
  6. double factor = System. Convert .ToDouble (Parameter);
  7. return Math .Pow (orbit / 40, 0.4) * 770 * factor;
  8. }
  9. public object ConvertBack ( object value , Type targetType, object parameter, CultureInfo culture)
  10. {
  11. throw new NotSupportedException ("This method should never be called");
  12. }
  13. }
* This source code was highlighted with Source Code Highlighter .

If you start the application now, you will see that all the planets are correctly located in relation to the sun. If you hover your mouse over them, you will get more detailed information about the planet. If you click on the planet, the standard ListBoxItem template will assign a blue background to the selected item, which looks like a small frame around the item. This is not the effect that I would like to see, so I decided to change the presentation of the selected element.
To change this style, I think it would be easier to use Microsoft Expression Blend to look at the standard template and then redo it the way you want it. I started by selecting the ListBox in Blend, then I proceeded to the β€œObject” menu, selected β€œEdit Other Styles”, β€œEdit ItemContainerStyle” and then β€œEdit a Copy”. Then I set a name for the template and clicked on β€œOK”. If at this stage you follow the β€œXAML” tab, then you will see the full standard style for ListBoxItem, which includes the following template:
  1. < Setter Property = ” Template ” >
  2. < Setter.Value >
  3. < ControlTemplate TargetType = ”{ x: Type ListBoxItem }” >
  4. < Border SnapsToDevicePixels = ” true ” x: Name = ” Bd ” Background = ”{ TemplateBinding Background }” ” BorderBrush =” { TemplateBinding BorderBrush } ” BorderThickness =” { TemplateBinding BorderThickness } ” Padding =” { TemplateBinding Padding } ” >
  5. < ContentPresenter SnapsToDevicePixels = ”{ TemplateBinding SnapsToDevicePixels }” HorizontalAlignment = ”{ TemplateBinding HorizontalContentAlignment }” VerticalAlignment = ”{ TemplateBinding VerticalContentAlignment }” />
  6. </ Border >
  7. < ControlTemplate.Triggers >
  8. < Trigger Property = ” IsSelected ” Value = ” true ” >
  9. < Setter Property = ” Background ” TargetName = ” Bd ” Value = ”{ DynamicResource { x: Static SystemColors . HighlightBrushKey }} ” />
  10. < Setter Property = ” Foreground ” Value = ”{ DynamicResource { x: Static SystemColors . HighlightTextBrushKey }} ” />
  11. </ Trigger >
  12. < MultiTrigger >
  13. < MultiTrigger.Conditions >
  14. < Condition Property = ” IsSelected ” Value = ” true ” />
  15. < Condition Property = ” Selector . IsSelectionActive ” Value =” false ” />
  16. </ MultiTrigger.Conditions >
  17. < Setter Property = ” Background ” TargetName = ” Bd ” Value = ”{ DynamicResource { x: Static SystemColors . ControlBrushKey }} ” />
  18. < Setter Property = ” Foreground ” Value = ”{ DynamicResource { x: Static SystemColors . ControlTextBrushKey }} ” />
  19. </ MultiTrigger >
  20. < Trigger Property = ” IsEnabled ” Value = ” false ” >
  21. < Setter Property = ” Foreground ” Value = ”{ DynamicResource { x: Static SystemColors . GrayTextBrushKey }} ” />
  22. </ Trigger >
  23. </ ControlTemplate.Triggers >
  24. </ ControlTemplate >
  25. </ Setter.Value >
  26. </ Setter >
* This source code was highlighted with Source Code Highlighter .

Using it as a basis, I created a simple template that adds a yellow ellipse around the selected planet:
  1. < Style TargetType = ” ListBoxItem ” >
  2. (...)
  3. < Setter Property = ” Template ” >
  4. < Setter.Value >
  5. < ControlTemplate TargetType = ”{ x: Type ListBoxItem }” >
  6. < Grid >
  7. < Ellipse x: Name = ” selectedPlanet ” Margin = ” -10 ” StrokeThickness = ” 2 ” />
  8. < ContentPresenter SnapsToDevicePixels = ”{ TemplateBinding SnapsToDevicePixels }”
  9. HorizontalAlignment = ”{ TemplateBinding HorizontalContentAlignment }”
  10. VerticalAlignment = ”{ TemplateBinding VerticalContentAlignment }” />
  11. </ Grid >
  12. < ControlTemplate.Triggers >
  13. < Trigger Property = ” IsSelected ” Value = ” true ” >
  14. < Setter Property = ” Stroke ” TargetName = ” selectedPlanet ” Value = ” Yellow ” />
  15. </ Trigger >
  16. </ ControlTemplate.Triggers >
  17. </ ControlTemplate >
  18. </ Setter.Value >
  19. </ Setter >
  20. </ Style >
* This source code was highlighted with Source Code Highlighter .

The following screenshot shows the final version of the application. If you hover your mouse over a picture of the planet, you will receive more detailed information about it. If you click on the planet, a yellow ellipse surrounds the planet.
')

Here you can find a project for Visual Studio with the code that was used in the article.

Source: https://habr.com/ru/post/43474/


All Articles