How to continuously scroll to bottom of scrollviewer MVVM after interruption - c#

I have previously used the Attached Property which was the top response to this question. I used it on a text block that was an output window for a background process. However I have notice that, when I scroll up inside the scroll viewer, the scroll viewer stops scrolling to the bottom.
I cannot figure out how to ensure that the scroll viewer continues scrolling to the bottom. Please could you suggest reasons why this might be happening or how I might go about rectifying this issue without code behind.

You can simply change the attached property to listen to changes in the property the TextBlock's Text is bound to, so whenever that changes your ScrollViewer will scroll to the bottom.
Usage:
<ScrollViewer HorizontalScrollBarVisibility="Auto" myApp:ScrollViewerAttachedProperties.ScrollToBottomOnChange="{Binding Logs}">
<TextBlock Text="{Binding Path=Logs}" />
</ScrollViewer>
The attached property:
public static class ScrollViewerAttachedProperties
{
public static readonly DependencyProperty ScrollToBottomOnChangeProperty = DependencyProperty.RegisterAttached(
"ScrollToBottomOnChange", typeof(object), typeof(ScrollViewerAttachedProperties), new PropertyMetadata(default(ScrollViewer), OnScrollToBottomOnChangeChanged));
private static void OnScrollToBottomOnChangeChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
var scrollViewer = dependencyObject as ScrollViewer;
scrollViewer?.ScrollToBottom();
}
public static void SetScrollToBottomOnChange(DependencyObject element, object value)
{
element.SetValue(ScrollToBottomOnChangeProperty, value);
}
public static object GetScrollToBottomOnChange(DependencyObject element)
{
return element.GetValue(ScrollToBottomOnChangeProperty);
}
}

Related

Change propery of control, that has a TwoWay binding

I have a custom control with bool property. The property is binding include template control with Popup.
XAML control:
<controls:AutoCompleteTextBox x:Name="PART_Editor"
IsEnabled="False"
IsPopupOpen="{Binding IsAutocompletePopupOpen}" />
Property in the control:
public bool IsPopupOpen
{
get => (bool)GetValue(IsPopupOpenProperty);
set => SetValue(IsPopupOpenProperty, value);
}
public static readonly DependencyProperty IsPopupOpenProperty =
DependencyProperty.Register("IsPopupOpen", typeof(bool), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
Binding to element include in template control:
<Popup x:Name="PART_AutoCompletePopup"
IsOpen="{Binding IsPopupOpen, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" />
I need change property IsPopupOpen on click. I decided to do this in behavior, but I need my control to be disabled. Therefore I was add behavior to container of control
<Grid>
<controls:AutoCompleteTextBox x:Name="PART_Editor"
IsEnabled="False"
IsPopupOpen="{Binding IsAutocompletePopupOpen}"/>
<i:Interaction.Behaviors>
<behaviors:PopupContainerBehavior IsPopupOpen="{Binding IsAutocompletePopupOpen, Mode=TwoWay}" />
</i:Interaction.Behaviors>
</Grid>
Behavior code:
public class PopupContainerBehavior : Behavior<UIElement>
{
public bool IsPopupOpen
{
get { return (bool)GetValue(IsPopupOpenProperty); }
set { SetValue(IsPopupOpenProperty, value); }
}
public static readonly DependencyProperty IsPopupOpenProperty =
DependencyProperty.Register("IsPopupOpen", typeof(bool), typeof(PopupContainerBehavior), new PropertyMetadata(false));
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.PreviewMouseLeftButtonDown += OnMouseLeftButtonUp;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.PreviewMouseLeftButtonDown -= OnMouseLeftButtonUp;
}
private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
IsPopupOpen = true;
}
}
The problem is that the property first changes to true and then immediately changes to false.Through SNOOP you can see this by the flashing value of the property.I think the problem lies in TwoWay Binding, but I don't know how to fix it
The reason why this is happening is due to mouse capture.
The behavior's PreviewMouseLeftButtonDown event handler tells the Popup to open
The Popup captures the mouse
The rest of the click events fire for the container
The container takes away mouse capture from the Popup
The Popup immediately closes
This can be a tricky problem to solve.
You might want to consider making your control able to open the Popup when it is disabled, vs. trying to do it from the outside. Even though a control is disabled, you can make parts of it clickable by setting IsHitTestVisible on the parts you need to be interactive, etc.

WPF How to unregister attached behavior

I am writing an application in WPF. I have a setup similar to that described by Sheridan in https://stackoverflow.com/a/19654812.
Specifically I have a listview that has a select command that sets the Mainview property on my RootViewModel to that of the selected entry. That is displayed in the following XAML
<DockPanel>
<UserControl DockPanel.Dock="Top" Content="{Binding MenuView,Mode=OneWay}"/>
<Border DockPanel.Dock="Top" BorderThickness="0 2 0 0" BorderBrush="{StaticResource GarminDarkBrush}" Panel.ZIndex="1001">
<DockPanel LastChildFill="True">
<UserControl DockPanel.Dock="Left" Content="{Binding ExpandableLeftPanel,Mode=OneWay}" />
<UserControl Content="{Binding MainView,Mode=OneWay}" Panel.ZIndex="0" />
</DockPanel>
</Border>
</DockPanel>
And here is a portion of that RootViewModel
public class RootViewModel : ViewModelBase
{
public virtual MenuViewModel MenuView { get; set; }
public virtual ViewModelBase MainView { get; set; }
}
This works for displaying different entries.
Now to my problem. In each of the viewmodels I display there is a child ConsoleConstrollerViewModel that has a ConsoleViewModel that when displayed registers an AttachedBehavior to a RichTextBox. When I select each item in my list the proper view is displayed and the ConsoleViewModel registers and behaves correctly. However, when I switch between the views that behavior is never unregistered. This causes a problem because whenever I switch views a new RichTextBox is created(by WPF not me) and bound to my behavior. Which would normally cause a memory leak because I attach event handlers to the RTB. I fixed this memory leak by using a dictionary of ConsoleViewModel and swapping out the RTB associated with the ConsoleViewModel via the ConsoleMediator class.
I would still like to understand how to properly unregister the RTB so that when swapping between views I can unregister other event handlers on the ConsoleViewModel that wastes CPU changing the RTB that is not displayed and will be replaced when viewed anyways.
Portion of the ConsoleControllerViewModel
public class ConsoleControllerViewModel : ViewModelBase
{
public virtual ConsoleViewModel Console { get; set; }
}
Here is the attached behavior
public static class ConsoleTextBoxBehavior
{
public static readonly DependencyProperty ConsoleBindingProperty =
DependencyProperty.RegisterAttached("Console",
typeof(ConsoleViewModel), typeof(ConsoleTextBoxBehavior),
new UIPropertyMetadata(null, OnConsoleBound));
public static ConsoleViewModel GetConsole(DependencyObject obj)
{
return (ConsoleViewModel)obj.GetValue(ConsoleBindingProperty);
}
public static void SetConsole(DependencyObject obj, ConsoleViewModel value)
{
obj.SetValue(ConsoleBindingProperty, value);
}
private readonly static Dictionary<ConsoleViewModel, ConsoleMediator> _registeredConsoles =
new Dictionary<ConsoleViewModel, ConsoleMediator>();
private static void OnConsoleBound(object sender, DependencyPropertyChangedEventArgs e)
{
var textBox = sender as RichTextBox;
if(textBox == null)
{
throw new ArgumentException("Can only bind to RichTextBoxes");
}
var c = e.OldValue as ConsoleViewModel;
if (e.OldValue != null && _registeredConsoles.ContainsKey(c))
{
// never hit
var cm = _registeredConsoles[c];
cm.UnhookConsole();
_registeredConsoles.Remove(c);
}
c = e.NewValue as ConsoleViewModel;
if (c != null)
{
HookUpConsole(textBox, c);
}
}
//other functions left out for brevity
}
Here it is attached to RTB. Please Note that {Binding .} works because This is from the context of a ConsoleViewModel not a ConsoleControllerViewModel.
<RichTextBox Name="ConsoleOutput"
ext:ConsoleTextBoxBehavior.Console="{Binding .}"
IsReadOnly="True"
>
I understand that OnConsoleBound should be called when notified of that particular property changing. In my particular application I never need that ConsoleViewModel to change with respect to its parent. So do I have to hack up my viewmodel to listen for events on the rootview to determine if it is the one that is actually being displayed? And then switch that Viewmodel out into a temporary place and then put it back when displayed? Seems ugly to me so i wanted to ask the peanut gallery for another solution before making a mess that is hard to untangle.
Also, ViewModelBase is a INotifyPropertyChanged and I have some magic(Dynamic Proxy) to raise those events for virtual members. So please don't get hung up on not seeing property changed events being raised.

Make active tab visible in scrollviewer

I have a lot of tabs in my tabcontrol, so I used the solution from want to make scrollable tabs for a tabcontrol.
The problem is that in my window I have buttons Previous-Next, that change active tab. So I want to scrollviewer to move automatically to active tab. It is possible to make by using BringIntoView() method of a FrameworkElement. But how can I implement it in my case?
here you go, I solved the issue by creating an attached behavior (Attached Properties)
if your template resembles as in the link you've posted
add the following style with the binding the attached property ScrollHelper.SelectScroll to IsSelected of the tab item
<TabControl>
<TabControl.Resources>
<Style TargetType="TabItem" xmlns:l="clr-namespace:CSharpWPF">
<Setter Property="l:ScrollHelper.SelectScroll"
Value="{Binding IsSelected,RelativeSource={RelativeSource Self}}" />
</Style>
</TabControl.Resources>
...
</TabControl>
behavior class
namespace CSharpWPF
{
class ScrollHelper : DependencyObject
{
public static bool GetSelectScroll(DependencyObject obj)
{
return (bool)obj.GetValue(SelectScrollProperty);
}
public static void SetSelectScroll(DependencyObject obj, bool value)
{
obj.SetValue(SelectScrollProperty, value);
}
// Using a DependencyProperty as the backing store for SelectScroll. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectScrollProperty =
DependencyProperty.RegisterAttached("SelectScroll", typeof(bool), typeof(ScrollHelper), new PropertyMetadata(false, OnSelectScroll));
private static void OnSelectScroll(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TabItem tab = d as TabItem;
if ((bool)e.NewValue)
{
tab.BringIntoView();
}
}
}
}
upon change of the property it will invoke BringIntoView() method which will pull the tab into view hence scrollviewer will scroll to the tab
you may choose to rename the property or the class as per your liking, I simply chose a random name what came to my mind.

Windows Store App - XAML - Bind two values for Height Property

I want to bind the height of a control the sum of two other heights, so the UI looks nice various screen sizes.
<GridView
AutomationProperties.AutomationId="ItemDetails"
ItemsSource="{Binding data}"
IsSwipeEnabled="False"
SelectionMode="None" Height="{Binding Height, (ElementName=item - ElementName=itemTitle)}" >
<GridView.ItemTemplate>
<DataTemplate>
<dll:TaskItemControl/>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
The above XAML is invalid, but it demonstrates what I want to do. I have two elements, item and itemTitle. item is a ScrollView that gets set to the height of the screen and I want the GridView to be the same height as the ScrollView minus the height of the itemTitle.
Is there a way to do this in XAML?
Note: The reasons for doing this are beyond the scope of this question. So please don't comment about restricting the height of a control within a ScrollView.
This can be easily done in code behind by subscribing the SizeChanged events of the two elements and update the Height of the GridView whenever the handlers are called.
But you want a pure XAML solution, and this is where Behaviors come into play.
Use a Behavior.
First you need to add Blend SDK reference to your project.
Then you need to create a new class that implements IBehavior. This class needs three dependency properties just to reference the GridView, the item and the itemTitle. So you can subscribe to their SizeChanged events and calulate the Height accordingly.
I choose to attach this behavior to a top level Panel (most likely your LayoutRoot Grid) because I want to ensure that all the elements under it are rendered properly inside its Loaded event handler.
The full Behavior class would look something like this -
public class HeightBehavior : DependencyObject, IBehavior
{
public GridView GridView
{
get { return (GridView)GetValue(GridViewProperty); }
set { SetValue(GridViewProperty, value); }
}
public static readonly DependencyProperty GridViewProperty =
DependencyProperty.Register("GridView", typeof(GridView), typeof(HeightBehavior), new PropertyMetadata(null));
public FrameworkElement FirstItem
{
get { return (FrameworkElement)GetValue(FirstItemProperty); }
set { SetValue(FirstItemProperty, value); }
}
public static readonly DependencyProperty FirstItemProperty =
DependencyProperty.Register("FirstItem", typeof(FrameworkElement), typeof(HeightBehavior), new PropertyMetadata(null));
public FrameworkElement SecondItem
{
get { return (FrameworkElement)GetValue(SecondItemProperty); }
set { SetValue(SecondItemProperty, value); }
}
public static readonly DependencyProperty SecondItemProperty =
DependencyProperty.Register("SecondItem", typeof(FrameworkElement), typeof(HeightBehavior), new PropertyMetadata(null));
public DependencyObject AssociatedObject { get; set; }
public void Attach(DependencyObject associatedObject)
{
this.AssociatedObject = associatedObject;
var control = (Panel)this.AssociatedObject;
control.Loaded += AssociatedObject_Loaded;
}
private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
{
this.FirstItem.SizeChanged += FirstItem_SizeChanged;
this.SecondItem.SizeChanged += SecondItem_SizeChanged;
// force to re-calculate the Height
this.FirstItem.Width += 0.5;
}
private void FirstItem_SizeChanged(object sender, SizeChangedEventArgs e)
{
this.SetAssociatedObjectsHeight();
}
private void SecondItem_SizeChanged(object sender, SizeChangedEventArgs e)
{
this.SetAssociatedObjectsHeight();
}
private void SetAssociatedObjectsHeight()
{
this.GridView.Height = this.FirstItem.ActualHeight - this.SecondItem.ActualHeight;
}
public void Detach()
{
this.FirstItem.SizeChanged -= FirstItem_SizeChanged;
this.SecondItem.SizeChanged -= SecondItem_SizeChanged;
var control = (Panel)this.AssociatedObject;
control.Loaded -= AssociatedObject_Loaded;
}
}
Then in my XAML, I attach it to my top level Grid, like this.
<Grid x:Name="LayoutRoot">
<Interactivity:Interaction.Behaviors>
<local:HeightBehavior GridView="{Binding ElementName=itemGridView}" FirstItem="{Binding ElementName=item}" SecondItem="{Binding ElementName=itemTitle}"/>
</Interactivity:Interaction.Behaviors>
Hope this helps.

UserControl using parent elements in wpf?

When you have a usercontrol in wpf can it reach outside to its parent elements? For instance my user control just lists some generic things which are held inside the control which is encapsulated within a dockpanel on the main window, but I have a textbox and button in the main window that I would like to access from the control... is this possible?
It would save me alot of time rather than changing the content of the entire window and displaying the same textbox/button in every usercontrol. If anyone has an example of this it would be much appreciated.
Yes it is possible and here is some code I have used to compose presentations out of UserControls that have DPs.
I don't love it even a little, but it works. I also think this is a great topic and maybe some code will help get some better answers!
Cheers,
Berry
UserControl XAML
<Button x:Name="btnAddNewItem" Style="{StaticResource blueButtonStyle}" >
<StackPanel Orientation="Horizontal">
<Image Source="{resx:Resx ResxName=Core.Presentation.Resources.MasterDetail, Key=bullet_add}" Stretch="Uniform" />
<Label x:Name="tbItemName" Margin="5" Foreground="White" Padding="10, 0">_Add New [item]</Label>
</StackPanel>
</Button>
UserControl Code Behind
public partial class AddNewItemButton : UserControl
{
...
#region Item Name
public static readonly DependencyProperty ItemNameProperty = DependencyProperty.Register(
"ItemName", typeof(string), typeof(AddNewItemButton),
new FrameworkPropertyMetadata(OnItemNameChanged));
public string ItemName
{
get { return (string)GetValue(ItemNameProperty); }
set { SetValue(ItemNameProperty, value); }
}
public string ButtonText { get { return (string) tbItemName.Content; } }
private static void OnItemNameChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
// When the item name changes, set the text of the item name
var control = (AddNewItemButton)obj;
control.tbItemName.Content = string.Format(GlobalCommandStrings.Subject_Add, control.ItemName.Capitalize());
control.ToolTip = string.Format(GlobalCommandStrings.Subject_Add_ToolTip, control.ItemName);
}
#endregion
#region Command
public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(
"Command", typeof(ICommand), typeof(AddNewItemButton),
new FrameworkPropertyMetadata(OnCommandChanged));
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
private static void OnCommandChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
// When the item name changes, set the text of the item name
var control = (AddNewItemButton)obj;
control.btnAddNewItem.Command = control.Command;
}
#endregion
}
Another UserControl showing Composition
<UserControl ...
xmlns:uc="clr-namespace:Smack.Core.Presentation.Wpf.Controls.UserControls"
>
<DockPanel LastChildFill="True">
...
<uc:AddNewItemButton x:Name="_addNewItemButton" Margin="0,0,10 0" DockPanel.Dock="Right" />
...
</DockPanel>
</UserControl>
A better design pattern would be to have the usercontrol notify (via event) the main window when something needs to be changed, and to ask the window (via method) when it needs some information. You would, for example, have a GetText() method on the window that the usercontrol could call, and a ChangeText event on the usercontrol that the window would subscribe to.
The idea is to keep the window in control at all times. Using this mentality will make it easier for you to develop applications in the future.
To answer your question: yes, you can either access parent controls either through a RelativeSource binding or through the Parent member in the back code. But a better answer is similar to #KendallFrey answer. Adopt a framework like Light MVVM and use its messenger class or use events the way Kendall described.

Categories

Resources