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
Related
I have built the following control:
MyContentControl.xaml.cs:
public partial class MyContentControl : UserControl
{
public MyContentControl()
{
InitializeComponent();
}
public static readonly DependencyProperty MyContentProperty = DependencyProperty.Register(
"MyContent", typeof(object), typeof(MyContentControl), new PropertyMetadata(default(object)));
public object MyContent
{
get { return (object) GetValue(MyContentProperty); }
set { SetValue(MyContentProperty, value); }
}
}
MyContentControl.xaml:
<UserControl x:Class="MyContentControl"
x:Name="self"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<Border BorderBrush="Gray" BorderThickness="1">
<ContentPresenter Content="{Binding ElementName=self, Path=MyContent}"/>
</Border>
</Grid>
</UserControl>
And it can be used like this:
<controls:MyContentControl>
<controls:MyContentControl.MyContent>
<TextBox Text="Some text..."/>
</controls:MyContentControl.MyContent>
</controls:MyContentControl>
What I would like to achieve is being able to use my control like this:
<controls:MyContentControl>
<TextBox Text="Some text..."/>
</controls:MyContentControl>
I would like to define the inner content like I would e.g. for a StackPanel.
How can this be achieved?
You need to decorate the MyContentControl class with the ContentPropertyAttribute https://learn.microsoft.com/de-de/dotnet/api/system.windows.markup.contentpropertyattribute?view=net-5.0
[ContentProperty("MyContent")]
public partial class MyContentControl : UserControl
{
...
Then you should be able to directly add the content without explicitly specifying "<controls:MyContentControl.MyContent>" in Property Element syntax. So that the markup below should parse and be valid:
<controls:MyContentControl>
<TextBox Text="Some text..."/>
</controls:MyContentControl>
Although lidqy's answer was closest to what I had initially intended, I ended up following Andy's suggestion from the comments to the question.
The key to that approach is: the desired custom XAML is defined in a style/template, in any available resource dictionary:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:MyControls">
<Style TargetType="{x:Type controls:MyContentControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:MyContentControl}">
<Grid>
<Border BorderBrush="Gray" BorderThickness="1">
<ContentPresenter/>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
The MyContentControl class then only needs to inherit ContentControl - and does not need to be a UserControl (with .xaml and xaml.cs) files.
#Andy: Regarding the question of "named elements": the exception/error is "Error MC3093: Cannot set Name attribute value 'xxx' on element 'yyy'. 'yyy' is under the scope of element 'MyContentControl', which already had a name registered when it was defined in another scope.", pretty much like here.
#lidqy: I also thought "why not just use the Content property?", but when trying I apparently created some kind of recursive binding.
Thank you, both! :o)
NOTE: The above approach should work for all ContentControls, including UserControls, enabling re-templating of basically any custom control ;o)
In WPF and C# I am trying to set up a mouse drag scrolling feature for a ScrollViewer contained within a control template for a document viewer. The problem: I have not been able to get the MouseLeftButtonEvent to fire.
It is basically the default DocumentViewer template, with a few features modified. Here is an outline in XAML:
<Style x:Key="DocumentViewerStyle1" BasedOn="{x:Null}" TargetType="{x:Type DocumentViewer}">
<!--...—>
<Setter Property="ContextMenu" Value="{x:Null}" /> <!--So does not mess up right click, if I use that-->
<!--...-->
<ControlTemplate TargetType="{x:Type DocumentViewer}">
<!--...-->
<ScrollViewer x:Name="PART_ContentHost" CanContentScroll="True"
IsHitTestVisible="True" HorizontalScrollBarVisibility="Auto"
Grid.Row="1" Loaded ="OnScrollViewerLoaded" />
<DockPanel Grid.Row="1" >
<!-...-->
</ControlTemplate>
</Style>
I use the following in code behind so that I can access the ScrollViewer. If one changes “Left” to “Right” in the method below, it works to perfection, but of course with the right mouse button rather than the left.
public partial class PrintPreview : Window
{
private ScrollViewer nomad;
etc. and
private void OnScrollViewerLoaded(object sender, RoutedEventArgs e)
{
nomad = (ScrollViewer)sender;
nomad.AddHandler(MouseLeftButtonDownEvent, new MouseButtonEventHandler(OnMouseButtonDown), true);
nomad.AddHandler(MouseLeftButtonUpEvent, new MouseButtonEventHandler(OnMouseButtonUp), true);
nomad.AddHandler(MouseMoveEvent, new MouseEventHandler(OnMouseMove), true);
}
The OnMouseButtonUp event handler, for example, is
private void OnMouseButtonUp(object sender, MouseButtonEventArgs e)
{
nomad.Cursor = Cursors.IBeam;
nomad.ReleaseMouseCapture();
}
Have tried various things found here: No help from using Preview events for my three mouse events. No help from setting Focusable="False" for the ScrollViewer, or for setting a Background for it. Any suggestions? Thanks!
I would like to create a custom control that provides all the functionality of the DockPanel, but it also exposes a secondary Overlay that is "outside" of the DockPanel. There would be a dependency property that will control the visibility of the the overlay panel, such that when the property is set true/visible, the Panel will appear overlayed on top of everything within the DockPanel.
Ideally the consumer would be able to drop the control into the same situation as a normal DockPanel, and with no other changes it would behave just like the normal DockPanel:
<DockPanelWithOverlay LastChildFill="True" >
<Button DockPanel.Dock="Bottom".../>
<Button DockPanel.Dock="Top".../>
<Grid>
<Grid controls.../>
</Grid>
</DockPanelWithOverlay>
However, there would be available the secondary area into which they could place the additional content and invoke when required.
<DockPanelWithOverlay LastChildFill="True" >
<Button DockPanel.Dock="Bottom".../>
<Button DockPanel.Dock="Top".../>
<Grid>
<Grid controls.../>
</Grid>
<DockPanel.Overlay>
<whatever controls for the overlay>
</DockPanel.Overlay>
</DockPanelWithOverlay>
But that wouldn't be valid since the Content is being set twice? So to cope, when using the overlay I guess I would have to explicitly state what goes where?:
<DockPanelWithOverlay LastChildFill="True" >
<DockPanel.Children>
<Button DockPanel.Dock="Bottom".../>
<Button DockPanel.Dock="Top".../>
<Grid>
<Grid controls.../>
</Grid>
</DockPanel.Children>
<DockPanel.Overlay Visibility="{Binding IsVisible}">
<whatever controls for the overlay>
</DockPanel.Overlay>
</DockPanelWithOverlay>
I'm not entirely sure the best way to tackle this: whether to create a CustomControl, or a UserControl, inherit directly from the DockPanel and try to expose a separate ContentControl, or maybe inherit from Panel and delegate the MeasureOverride and ArrangeOverride to the DockPanel.
How should I tackle this?
Interesting question. I wrote a DockPanelWithOverlay component that does the work:
I chose here the CustomControl because I wanted to have inheritance of Panel.
But Panel doesn't have a template it can change.
So I wrote a Custom Control inheriting of Control with a custom template
But a Usercontrol would perfectly work I think (I didn't try to be honest)
Edit UserControl is not so good, because it inherits of ContentControl.
So it can only have one children.
The goal of the DockPanelWithOverlay is to have many children.
So I think UserControl is not the best inheritance, as often.
UserControl is better when you want to provide some content in xaml, mostly static, not customizable by user of control.
End of edit
To organize content inside the tempalte, I used a Grid.
Order of the two components matters.
It is the drawing order.
Grid allows to put two components at the same place :
Inside there'll be the Overlay control, and a underlying DockPanel.
DockPanelWithOverlay
..|
..|-ControlTemplate
......|
......|-Grid
..........|
..........|--DockPanel
..........|--OverlayControl
Having a template is easier to make some binding from the DockPanelWithOverlay to the template's controls properties.
(To generate a CustomControl, create a WPFCustom Control Library Project)
Excerpt of themes\generic.xaml in library :
<Style TargetType="{x:Type local:DockPanelWithOverlay}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:DockPanelWithOverlay}">
<!-- the grid allows to put two components at the same place -->
<Grid >
<DockPanel x:Name="dockPanel" />
<ContentControl x:Name="overlayControl" Visibility="{TemplateBinding OverlayVisibility}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Inheriting of control allows to use the template to create the small UIElements hierarchy.
Some dependency properties must be added for allowing binding :
Overlay for providing some UIElements, or a string for overlay content
OverlayVisibility for hiding/showing the overlay
Here is the code for the DockPanelWithOverlay :
(Note the OnApplytemplate called just after the templates componenets are called)
// Children is the property that will be valued with the content inside the tag of the control
[ContentProperty("Children")]
public class DockPanelWithOverlay : Control
{
static DockPanelWithOverlay()
{
// Associate the control with its template in themes/generic.xaml
DefaultStyleKeyProperty.OverrideMetadata(typeof(DockPanelWithOverlay), new FrameworkPropertyMetadata(typeof(DockPanelWithOverlay)));
}
public DockPanelWithOverlay()
{
Children = new UIElementCollection(this, this);
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
// once the template is instanciated, the dockPanel and overlayCOntrol can be found from the template
// and the children of DockPanelWithOverlay can be put in the DockPanel
var dockPanel = this.GetTemplateChild("dockPanel") as DockPanel;
if (dockPanel != null)
for (int i = 0; i < Children.Count; )
{
UIElement elt = Children[0];
Children.RemoveAt(0);
dockPanel.Children.Add(elt);
}
}
// Here is the property to show or not the overlay
public Visibility OverlayVisibility
{
get { return (Visibility)GetValue(OverlayVisibilityProperty); }
set { SetValue(OverlayVisibilityProperty, value); }
}
// Here is the overlay. Tipically it could be a Texblock,
// or like in our example a Grid holding a TextBlock so that we could put a semi transparent backround
public Object Overlay
{
get { return (Object)GetValue(OverlayProperty); }
set { SetValue(OverlayProperty, value); }
}
// Using a DependencyProperty as the backing store for OverlayProperty.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty OverlayProperty =
DependencyProperty.Register("Overlay", typeof(Object), typeof(DockPanelWithOverlay), new PropertyMetadata(null));
public static readonly DependencyProperty OverlayVisibilityProperty =
DependencyProperty.Register("OverlayVisibility", typeof(Visibility), typeof(DockPanelWithOverlay), new PropertyMetadata(Visibility.Visible));
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public UIElementCollection Children
{
get { return (UIElementCollection)GetValue(ChildrenProperty); }
set { SetValue(ChildrenProperty, value); }
}
public static readonly DependencyProperty ChildrenProperty =
DependencyProperty.Register("Children", typeof(UIElementCollection), typeof(DockPanelWithOverlay), new PropertyMetadata(null));
}
Using the DockPanelWithOverlay :
<lib:DockPanelWithOverlay x:Name="dockPanelWithOverlay1"
OverlayVisibility="Visible"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Button Content="Top" Height="50" DockPanel.Dock="Top" Background="Red"/>
<Button Content="Bottom" Height="50" DockPanel.Dock="Bottom" Background="Yellow"/>
<Button Content="Left" Width="50" DockPanel.Dock="Left" Background="Pink"/>
<Button Content="Right" Width="50" DockPanel.Dock="Right" Background="Bisque"/>
<Button Content="Center" Background="Azure"/>
<lib:DockPanelWithOverlay.Overlay>
<Grid Background="#80404080">
<TextBlock Text="Overlay" FontSize="80" Foreground="#FF444444" HorizontalAlignment="Center" VerticalAlignment="Center" RenderTransformOrigin="0.5,0.5">
<TextBlock.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform Angle="-15"/>
<TranslateTransform/>
</TransformGroup>
</TextBlock.RenderTransform>
</TextBlock>
</Grid>
</lib:DockPanelWithOverlay.Overlay>
</lib:DockPanelWithOverlay>
The overlay can easily be switched on or off binding from a CheckBox.IsChecked property for instance.
Here is the full working code : http://1drv.ms/1NfCl9z
I think it 's really the answer to your question. Regards
I suggest we should try to clarify how you see this working. My guess is that the secondary panel will be a DockPanel too, and will completely cover the main panel. I.e., you see either the one or the other, but never both. How do you envisage switching between the two? A ToggleButton perhaps? Or only under control of, say, some Trigger?
My first thought as to implementation is that you seem to like how DockPanel? lays things out, so why touch the layout methods? One way might be to have only one dockpanel, but two collections of children, which you set according to which you want to show. Or the secondary panel in a `Popup?
Do you want to be able to write something like this:
<DockPanelWithAlternative
AlternativeVisibility="{Binding somethingHere}" >
<TextBlock Dock.Top ... />
<TextBlock Dock.Alternative.Top ... />
</DockPanelWithAlternative>
What I am thinking of is something like:
<UserControl>
<Grid>
<DockPanel x:Name="MainPanel" ZIndex="0"/>
<DockPanel x:Name="AlternativePanel" Visbility=... ZIndex="1"/>
</Grid>
</UserControl>
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.
How do I, form my contructor in the code-behind get a reference to the OuterBorder control in the XAML below?
<Window Template="{DynamicResource WindowTemplate}">
<Window.Resources>
<ControlTemplate x:Key="WindowTemplate" TargetType="{x:Type Window}">
<AdornerDecorator>
<Border Name="OuterBorder" Background="Black" BorderBrush="Red" BorderThickness="1" CornerRadius="0">
<!-- Implementation here... -->
</Border>
</AdornerDecorator>
</ControlTemplate>
</Window.Resources>
</Window>
Two possible solutions:
Solution 1
Put a Loaded event in XAML
<Border Name="OuterBorder" Loaded="Border_Loaded" ...
And in code behind store it in a private field:
private Border border;
void Border_Loaded(object sender, RoutedEventArgs e)
{
this.border = (Border)sender;
}
OR:
Solution 2
Override the OnApplyTemplate of your Window:
private Border border;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.border = (Border) Template.FindName("OuterBorder", this);
}
You may want to reconsider your approach. What are you trying to do?
Generally, you shouldn't want or need to access portions of the ControlTemplate from your codebehind because your template is just that-- a template. It's how the control looks. You want your codebehind to generally affect the behavior of the control.
For example, if you're trying to affect the color of the border in the codebehind in certain interactive situations, you really want to add some (pre .Net4) triggers or (post .Net4) a VisualStateManager to your control template to manage your control's visual states for you.