Note: I know I should use {Binding RelativeSource={RelativeSource TemplatedParent}} to resolve the presented issue but I want to know why the issue appears.
I have a custom control CustomTextControl with a ControlTemplate where I use RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type local:CustomTextControl}} and when I display it inside a Window, everything is displayed as expected.
But when I try to print the same control with the PrintVisual method of System.Windows.Controls.PrintDialog, the binding is not evaluated correctly.
Example:
public class CustomTextControl : ContentControl
{
static CustomTextControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomTextControl), new FrameworkPropertyMetadata(typeof(CustomTextControl)));
}
}
Generic.xaml (local refers to the xmlns:local definition of my project namespace):
<Style TargetType="{x:Type local:CustomTextControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:CustomTextControl}">
<TextBlock
Background="Red"
Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type local:CustomTextControl}},Path=Content}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
MainWindow content:
<Grid>
<local:CustomTextControl Content="My text to be displayed" VerticalAlignment="Top" HorizontalAlignment="Left" Margin="10"/>
<Button Content="PrintTest" Click="Button_Click" VerticalAlignment="Top" HorizontalAlignment="Left" Margin="10,100,10,10"/>
</Grid>
The control is displayed with red background and the specified text content.
MainWindow code behind:
private void Button_Click(object sender, RoutedEventArgs args)
{
var e = new CustomTextControl();
e.Content = "My text to be printed";
e.Margin = new Thickness(30);
e.UpdateLayout();
var print = new PrintDialog();
if (print.ShowDialog() != true) return;
var w = print.PrintTicket.PageMediaSize.Width ?? 600;
var h = print.PrintTicket.PageMediaSize.Height ?? 1000;
e.Measure(new Size(w, h));
e.Arrange(new Rect(0, 0, w, h));
print.PrintVisual(e, "Test Printing");
}
As you see, I create a separate control that's sufficiently similar to the control inside the mainwindow.
Result: the printed document contains the red background but not the text content.
My question: why is the text content displayed in the window but not in the printed document?
Update
If I place a MessageBox.Show("Test"); right before the print.PrintVisual(e, "Test Printing");, then the printed document has red background, so it is some sort of work/timing issue.
I was able to resolve my specific example by using Dispatcher.BeginInvoke on the printing statement but for a little modified example involving a Data-Bound CheckBox, this was not enough, even with DispatcherPriority.ApplicationIdle.
I think this happens, because your control isn't in visual tree. RelativeSource binding searches in parent of current control.
You can try to replace your RelativeSource with TemplateBinding inside Template definition.
Try to Use
Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type local:CustomTextControl}},Path=Content}"/>
instead of:
Text="{TemplateBinding Content}"/>
Related
In dotnet's Avalonia-UI framework.
I'm using a dark UI and I managed to make everything dark as per this example but one thing: the window's System top bar in Windows OS.
I have seen in this issue in github that I can set the property HasSystemDecorations="false" to make it go away, but then I would have to implement myself the top bar with the drag functionality, title, close, maximize, minimize, etc, which is a pain when all I want is to change the background color.
What would be the easier way to make the window top bar change to a dark background color?
If the only way is using HasSystemDecorations then what would be the minimal example to implement the dark top bar with the common funcionality to close/minimize/maximize/drag?
Yes, you have to set HasSystemDecorations="false" and implement your own title bar. I have a basic template on Github for how to do this using version 0.10 and fluent theme.
It is actually quite easy, because Avalonia provides a lot of convenience methods for achieving that.
Overview:
Set
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaChromeHints="NoChrome"
ExtendClientAreaTitleBarHeightHint="-1"
for the Window and then implement a titlebar. For example the close button could look something like this:
<Button Width="46"
VerticalAlignment="Stretch"
BorderThickness="0"
Name="CloseButton"
ToolTip.Tip="Close">
<Button.Resources>
<CornerRadius x:Key="ControlCornerRadius">0</CornerRadius>
</Button.Resources>
<Button.Styles>
<Style Selector="Button:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Red"/>
</Style>
<Style Selector="Button:not(:pointerover) /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent"/>
</Style>
<Style Selector="Button:pointerover > Path">
<Setter Property="Fill" Value="White"/>
</Style>
<Style Selector="Button:not(:pointerover) > Path">
<Setter Property="Fill" Value="{DynamicResource SystemControlForegroundBaseHighBrush}"/>
</Style>
</Button.Styles>
<Path Margin="10,0,10,0"
Stretch="Uniform"
Data="M1169 1024l879 -879l-145 -145l-879 879l-879 -879l-145 145l879 879l-879 879l145 145l879 -879l879 879l145 -145z"></Path>
</Button>
If you set IsHitTestVisible="False" on a control, the window below can be dragged in that area. So wrap your whole titlebar in for example a DockPanel:
<DockPanel Background="Black"
IsHitTestVisible="False"
Name="TitleBarBackground"></DockPanel>
Now you obviously still need to mimic the behaviour of the buttons. This can be done in principal like that (again for a concrete example check out the Github repo above):
minimizeButton = this.FindControl<Button>("MinimizeButton");
maximizeButton = this.FindControl<Button>("MaximizeButton");
maximizeIcon = this.FindControl<Path>("MaximizeIcon");
maximizeToolTip = this.FindControl<ToolTip>("MaximizeToolTip");
closeButton = this.FindControl<Button>("CloseButton");
windowIcon = this.FindControl<Image>("WindowIcon");
minimizeButton.Click += MinimizeWindow;
maximizeButton.Click += MaximizeWindow;
closeButton.Click += CloseWindow;
windowIcon.DoubleTapped += CloseWindow;
private void CloseWindow(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
Window hostWindow = (Window)this.VisualRoot;
hostWindow.Close();
}
private void MaximizeWindow(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
Window hostWindow = (Window)this.VisualRoot;
if (hostWindow.WindowState == WindowState.Normal)
{
hostWindow.WindowState = WindowState.Maximized;
}
else
{
hostWindow.WindowState = WindowState.Normal;
}
}
private void MinimizeWindow(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
Window hostWindow = (Window)this.VisualRoot;
hostWindow.WindowState = WindowState.Minimized;
}
Now the last step is that you need to change the icon of the maximize button depending on the window state. For example if you drag a maximized window, it will automatically become restored down and the icon of the maximize button needs to change. Therefore you need to subscribe to the window state of your host window, which can be done by overriding the Window.HandleWindowStateChanged method or by doing something like this:
private async void SubscribeToWindowState()
{
Window hostWindow = (Window)this.VisualRoot;
while (hostWindow == null)
{
hostWindow = (Window)this.VisualRoot;
await Task.Delay(50);
}
hostWindow.GetObservable(Window.WindowStateProperty).Subscribe(s =>
{
hostWindow.Padding = hostWindow.OffScreenMargin;
if (s != WindowState.Maximized)
{
maximizeIcon.Data = Avalonia.Media.Geometry.Parse("M2048 2048v-2048h-2048v2048h2048zM1843 1843h-1638v-1638h1638v1638z");
maximizeToolTip.Content = "Maximize";
}
if (s == WindowState.Maximized)
{
maximizeIcon.Data = Avalonia.Media.Geometry.Parse("M2048 1638h-410v410h-1638v-1638h410v-410h1638v1638zm-614-1024h-1229v1229h1229v-1229zm409-409h-1229v205h1024v1024h205v-1229z");
maximizeToolTip.Content = "Restore Down";
}
});
}
Actually in the snippet above there is one more detail, which needs some attention. At least on windows, a maximized window is actually bigger than the screen. If you dont want your content to go out of the screens' bounds, you need to add a margin to your main control inside the window. Therefore the Padding of the hostWindow is changed accordingly.
Avalonia provides an IWindowImpl.OffScreenMargin property that describes the margin around the window that is offscreen.
You can directly bind to this property in the window's .axml
<Window xmlns="https://github.com/avaloniaui"
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"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaChromeHints="NoChrome"
ExtendClientAreaTitleBarHeightHint="-1"
Padding="{Binding $self.OffScreenMargin}">
There is a way without having to create your own minimize/maximize/close buttons (I only tested it on Windows).
In your MainWindow.axaml:
<Window xmlns="https://github.com/avaloniaui"
...
TransparencyLevelHint="AcrylicBlur"
Background="Transparent"
ExtendClientAreaToDecorationsHint="True"/>
<Grid RowDefinitions="30,*">
<!-- Title bar -->
<Grid ColumnDefinitions="Auto,*" IsHitTestVisible="False" Background="Black">
<Image Grid.Column="0" VerticalAlignment="Center" Source="/Assets/YOUR-PATH-TO-YOUR-APP-ICON-IMAGE" Width="18" Margin="12,0,12,0" ></Image>
<TextBlock Grid.Column="1" VerticalAlignment="Center" FontSize="12" >YOUR-APPLICATION-TITLE-HERE</TextBlock>
</Grid>
<!-- Window content -->
<Your-User-Content-Here Grid.Row="1" Background="#222222" />
</Grid>
Here is the example in the AvaloniaUI documentation.
Here is an example in a real project with a black system bar.
I've written a custom control that should display items in a list and provide additional commands related to scrolling events, like load more. So I decided to create a ScrollViewer and add the ItemsPresenter inside generic.xaml.
Basically this works fine when I use ItemsControl as base class. But now I need the possibility to click on a single item. The solution is to use the ListView class as base class.
Here comes the problem. As soon as I use a GridView or ListView as base class the content of the list is only shown as far as it is shown at inital offset of the list. other/new items that you can only see by scrolling down aren't shown.
I thought that the list maybe dont resize, but adding a footer and a border around the list shows the the list resizes correctly.
The collection I use is a ObservableCollection and works with the ItemsControl base class.
I think the problem is a setting or something on xaml side. But I dont know where and i have no ideas what to search next. everytime I search, all results are marked as clicked =(
Here is my xaml code of the generic.xaml:
<Style TargetType="lists:ListViewWithCommands">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="lists:ListViewWithCommands">
<Border
Background="{TemplateBinding Background}"
BorderBrush="Red"
BorderThickness="1">
<ScrollViewer x:Name="ItemScrollViewer">
<Border BorderBrush="White" BorderThickness="2">
<StackPanel>
<ItemsPresenter />
<ContentPresenter Visibility="{TemplateBinding LoadingTemplateVisibility}" ContentTemplate="{TemplateBinding LoadingTemplate}" />
</StackPanel>
</Border>
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Here is a part of my control itselve:
[TemplatePart(Name="ItemScrollViewer", Type=typeof(ScrollViewer))]
public sealed class ListViewWithCommands : ListView
{
private ScrollViewer _itemScrollViewer;
public ListViewWithCommands()
{
this.DefaultStyleKey = typeof(ListViewWithCommands);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
this._itemScrollViewer = GetTemplateChild("ItemScrollViewer") as ScrollViewer;
if (this._itemScrollViewer != null)
{
Debug.WriteLine(String.Format("ItemScrollViewer found! Attatching Event Handler!"), this.GetType().Name);
this._itemScrollViewer.ViewChanged += _itemScrollViewer_ViewChanged;
}
else
{
throw new NullReferenceException("ItemScrollViewer not found!");
}
}
....
I hope you have any suggestions.
robert
I have a stackpanel named "mystack" in my xaml file and I am adding buttons in it dynamically from the .cs file and want to remove the border of buttons in C# .cs file
what I really want is to populate this stackpanel with the buttons coming from a list of string
thanks in advance
xaml:
<Grid HorizontalAlignment="Left" Height="227" Margin="10,10,0,0" Grid.Row="2"
VerticalAlignment="Top" Width="530">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="mystack" HorizontalAlignment="Left" Grid.Row="2"
VerticalAlignment="Top" Width="520"/>
</ScrollViewer>
</Grid>
.cs:
public List<String> Schools()
{
List<String> l = new List<string>();
l.Add("SST");
l.Add("SBE");
l.Add("SSH");
return l;
}
I agree with HighCore, you generally do not want to manipulate the UI elements in your code.
To remove the Border of the buttons you can set a Button's BorderThickness property to "0" in Xaml or to new Thickness(0) in the code-behind.
i.e.
myButton.BorderThickness = new Thickness(0);
EDIT:
Okay, I noticed your updated question. I would create a property that stores your list of schools and bind to it in a way similar to this:
public List<string> Schools
{
get { return _schools; }
set { _schools = value; }
}
Somewhere you need to set the DataContext of the control to your class containing the Schools property. If you are dynamically updating the list of Schools you'll need to implement INotifyPropertyChanged so the UI knows when to update. And then your Xaml would look something like this:
<ItemsControl ItemsSource="{Binding Schools}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding}" BorderThickness="0" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl>
You can't remove button's border like: btn.BorderThicknes=new Thickness(0);
See this: Removing button's border
The fast Fix:
What I had to do to effectively hide the button border - and due to the button control template I believe which utilizes and changes Button border (i.e. even if you remove it it'd draw it on some trigger I believe)...
...was to set BorderBrush="Transparent" as well (I always do BorderThickness as well but I'm guessing it's not needed - only for visual/layout look'n'feel)
i.e. setting thickness alone is not enough.
I'm really not sure that's the bets way to do it, or actually I'm
quite sure there must be something smarter - but I always end up with
that.
The Right Way:
Proper way - and recommended - is to write your own Button template -
based on the Microsoft official one - or base it on it - and do what
you need w/o borders.
For the code behind/C#:
You really don't need that as per your changed question - do what others suggested already
the best way to do this is :
<Style TargetType="Button">
<Style.Resources>
<Style TargetType="{x:Type Border}">
<Setter Property="CornerRadius" Value="0"/>
</Style>
</Style.Resources>
</Style>
what I really want is to populate this stackpanel with the buttons
coming from a list of string
That's called a ListBox:
<ListBox ItemsSource="{Binding Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<Button Content="{Binding}" BorderThickness="0"/>
<!-- Whatever other customizations to the button -->
</DataTemplate
</ListBox.ItemTemplate>
</ListBox>
ViewModel:
public class ViewModel
{
public ObservableCollection<string> Items {get;set;}
public ViewModel()
{
Items = new ObservablecCollection<string>();
Items.Add("String1");
Items.Add("String2");
Items.Add("String3");
}
}
You need to learn the MVVM pattern.
Some days ago I've faced with strange behaviour of text inside Button (I guess the same behaviour I would got for other ContentControls). Let me explain the situation. I have a style definition in App.xaml for TextBlock:
<Application.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Margin" Value="10"/>
</Style>
</Application.Resources>
In MainWindow.xaml I have the same style definition, that should override style that defined in App.xaml. Also I have 3 buttons in Window. In first button explicitly defined TextBlock control inside button's content. For second button I set a string as content in codebehind. For third button I set an integer value as content in codebehind. Here is code of MainWindow.xaml:
<StackPanel>
<StackPanel.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Margin" Value="0"/>
</Style>
</StackPanel.Resources>
<Button Name="Button1">
<Button.Content>
<TextBlock Text="Button with text block"/>
</Button.Content>
</Button>
<Button Name="Button2" />
<Button Name="Button3" />
</StackPanel>
and MainWindow.xaml.cs:
private void Window_Loaded(object sender, RoutedEventArgs e)
{
Button2.Content = "Button with string";
Button3.Content = 16;
}
And now what we see? Text in first and third buttons, as expected, have margins 0px, but text in second button have margins 10px! The question is: why second button has 10px margins and how set zero margins for second button (removing style from App.xaml is not possible)?
Thank you!
When I change
Button2.Content = "Button with string";
to
Button2.Content = "Button with _string";
the button's margin changes from 10 to 0.
This is a bug in WPF; it already has been reported on Microsoft Connect.
I am not 100% sure but I think the behavior you saw is caused by the same root cause.
By the way: the correct behavior would be that buttons 2 and 3 have Margin=10; this is because resource lookup is performed along the logical tree, not along the visual tree. The TextBlocks in the buttons 2 and 3 are not inside the logical tree of the StackPanel.
I can't give you a definitive answer, but I notice that it's the difference between setting a string and an integer which causes the different styles to be applied.
Since setting Content to a value that requires a conversion results in correct style being applied, I tried this:
private void WindowLoaded(object sender, RoutedEventArgs e)
{
Button2.Content = new TextHolder("Button with string");
Button3.Content = 16;
}
public class TextHolder
{
private readonly string _text;
public TextHolder(string text)
{
_text = text;
}
public override string ToString()
{
return _text;
}
}
and the margin is now 0. I would be interested in understanding exactly what is going on.
I have a custom TextBox control. I am trying to bind it to a simple Description string property of an object. How can I get the binding to work? The same binding works fine if I change this to a TextBox.
<Style x:Key="{x:Type TaskDash:TextBoxWithDescription}" TargetType="{x:Type TaskDash:TextBoxWithDescription}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TaskDash:TextBoxWithDescription}">
<TextBox>
</TextBox>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
public class TextBoxWithDescription : TextBox
{
public TextBoxWithDescription()
{
LabelText = String.Empty;
}
public string LabelText { get; set; }
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var textBlock = (TextBlock)this.Template.FindName("LabelText", this);
if (textBlock != null) textBlock.Text = this.LabelText;
}
}
<TaskDash:TextBoxWithDescription Grid.Row="0" Grid.Column="0"
x:Name="textBoxDescription"
Text="{Binding Description, BindsDirectlyToSource=True}" LabelText="Description">
</TaskDash:TextBoxWithDescription>
public partial class EditTaskItem : Window
{
private TaskItem _taskItem;
public EditTaskItem(TaskItem taskItem)
{
InitializeComponent();
this.DataContext = taskItem;
textBoxDescription.DataContext = taskItem;
_taskItem = taskItem;
}
}
Ok ... there are a couple of things wrong with your code.
Lets begin with your style for your custom control. You need to add a static constructor which allows restyling your new control. Like this
static TextBoxWithDescription ()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(TextBoxWithDescription ), new FrameworkPropertyMetadata(typeof(TextBoxWithDescription )));
}
This tells WPF "Hey, please look for a Style for this control".
Now you can remove the x:Key="{x:Type TaskDash:TextBoxWithDescription}"because this is going to be your default style.
Next thing is. In WPF one thing to understand is, that every control has absolutely no UI content, unless it gets an Template. You already have a Template in your Style, but the only visual content it gets is an empty TextBox. This is strange, because you derive your TextBoxWithDescription from TextBox. So what you create is a a control derived from textbox, containing a textbox.
See this to see how a TextBox Template looks in WPF 4.0.
Back to your empty TextBox in your ControlTemplate. Remember that i said, your controls, without a style are completely invisible, its only logic. The only visible thing is the TextBox in your Template. To make it work somehow, you need to pass some properties to this TextBox. The control says how and the template takes the properties and puts them in use.
You can do this via TemplateBinding
For example. If your control has a Property Background, this property does nothing you can set it as long as you want, but a ControlTemplate can give some meaning to it. So for example add a Rectangle in your ControlTemplate and set the Fill Property to {TemplateBinding Background}. Which basicaly tells the Rectangle "Your property Fill is going to be the value of Background from the control we are currently templating". I hope this makes it clear.
Next thing: You overrid OnApplyTemplate you usually do this to find a control in your ControlTemplate by name. It seems you mixed this with one of your properties. To Find a control in your Template via FindName, you need to give it a name
Like this
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TaskDash:TextBoxWithDescription}">
<TextBox x:Name="PART_MyTextBox">
</TextBox>
</ControlTemplate>
</Setter.Value>
</Setter>
and modify your OnApplyTemplate to
var textBlock = (TextBlock)this.Template.FindName("PART_MyTextBox", this);
Now you have the textblock in your current ControlTemplate.
The last mistake i can see is.
You set in OnApplyTemplatethe TextBox Text to your LabelText. While this works, one time, it is not very nice and usually not the WPF way. Also if you modify the LabelText property, it will not be displayed, because you would have to set it again.
Change your LabelText to a dependency property Now you can use the already mentioned TemplateBinding to set this text, directly on your control template TextBox.
<TextBox x:Name="PART_MyTextBox" Text="{TemplateBinding LabelText}>
</TextBox>
This will also make sure that changes to your Control property, will also update the Text on this TextBox.
One last thing
this.DataContext = taskItem;
// textBoxDescription.DataContext = taskItem;
_taskItem = taskItem;
If your textboxdescription is going to be a parent of your window, you don't need to set the DataContext explicitly, because it will be inherited down the hierachy. As long as an Element don't change the DataContext, it will be always the DataContext of the parent.
I would suggest you read more on the basics of WPF, i know it has a steep learning curve, but its worth the effort. It is difficult if someone comes from a WinForms background to get the head wrapped around all the different new design philosophies and different ways to do things.
Hope that helps a bit
#dowhilefor - great answer. I write this as an answer only because it's going to be too long for a comment.
#Shawn - it seems like you are trying to make a Label-Field control. If that's the case, try this template:
<ControlTemplate TargetType="{x:Type TaskDash:TextBoxWithDescription}">
<Grid>
<Grid.ColumnDefinitions>
<!--The SharedSizeGroup property will allow us to align all text boxes to the same vertical line-->
<ColumnDefinition Width="Auto"
SharedSizeGroup="LabelColumn"/>
<!--This column acts as a margin between the label and the text box-->
<ColumnDefinition Width="5"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Text="{TemplateBinding LabelText}"
VerticalAlignment="Center"/>
<Border Grid.Column="2"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer x:Name="PART_ContentHost"
Padding="{TemplateBinding Padding}"/>
</Border>
</Grid>
</ControlTemplate>
And remove the override for OnApplyTemplate.
If the control is a part of a (often called) "PropertyGrid" and you have multiple instances of the TextBoxWithDescription control in the same panel, define Grid.IsSharedSizeScope on that panel (it doesn't have to be Grid panel). This will align the text boxes to a uniform vertical line.