How to create checkbox in xamarin forms - xamarin

I need a checkbox control in xamarin forms, and also the event when the control is check, how can I get this, I am using switch control, But its not having any event when IsChecked as changed, I need to group the checkbox, its not possible in switch control in xamarin forms

The Switch control has a Toggled event that will fire when the state is changed.
Forms does not have a Checkbox control because there is no underlying Checkbox control in each mobile platform for it to map to.
XF Labs (an open source set of extensions for XF) does have a beta checkbox control that may fit your needs.

public class CustomCheckbox : Image
{
private const string CheckboxUnCheckedImage = "checkbox_unchecked";
private const string CheckboxCheckedImage = "checkbox_checked";
public CustomCheckbox()
{
Source = CheckboxUnCheckedImage;
var imageTapGesture = new TapGestureRecognizer();
imageTapGesture.Tapped += ImageTapGestureOnTapped;
GestureRecognizers.Add(imageTapGesture);
PropertyChanged += OnPropertyChanged;
}
private void ImageTapGestureOnTapped(object sender, EventArgs eventArgs)
{
if (IsEnabled)
{
Checked = !Checked;
}
}
/// <summary>
/// The checked changed event.
/// </summary>
public event EventHandler<bool> CheckedChanged;
/// <summary>
/// The checked state property.
/// </summary>
public static readonly BindableProperty CheckedProperty = BindableProperty.Create("Checked", typeof(bool), typeof(CustomCheckbox), false, BindingMode.TwoWay, propertyChanged: OnCheckedPropertyChanged);
public bool Checked
{
get
{
return (bool)GetValue(CheckedProperty);
}
set
{
if (Checked != value)
{
SetValue(CheckedProperty, value);
CheckedChanged?.Invoke(this, value);
}
}
}
private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e?.PropertyName == IsEnabledProperty.PropertyName)
{
Opacity = IsEnabled ? 1 : 0.5;
}
}
private static void OnCheckedPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var checkBox = bindable as CustomCheckbox;
if (checkBox != null)
{
var value = newValue as bool?;
checkBox.Checked = value.GetValueOrDefault();
checkBox.Source = value.GetValueOrDefault() ? CheckboxCheckedImage : CheckboxUnCheckedImage;
}
}
}

Related

How to optimize xamarin Treeview performence?

So long story short, I wanted to create a treeview control but I got hit by a huge performence problem when there is many items. that was mainly because of 2 things, but I just want to ask you now for one.
So I have an ObservableCollection of type TreeViewNode that inherits from StackLayout (This is my treeView item source)
This the treeviewnode class :
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Text;
using Xamarin.Forms;
namespace Involys.PraxisDemo.Controls.InCustomTreeView
{
public class TreeViewNode : StackLayout
{
#region Image source for icons
private DataTemplate _ExpandButtonTemplate = null;
#endregion
#region Fields
private TreeViewNode _ParentTreeViewItem;
private DateTime _ExpandButtonClickedTime;
private readonly BoxView _SpacerBoxView = new BoxView();
private readonly BoxView _EmptyBox = new BoxView { BackgroundColor = Color.Blue, Opacity = .5 };
private const int ExpandButtonWidth = 32;
private ContentView _ExpandButtonContent = new ContentView();
private readonly Grid _MainGrid = new Grid
{
VerticalOptions = LayoutOptions.StartAndExpand,
HorizontalOptions = LayoutOptions.FillAndExpand,
RowSpacing = 2
};
private readonly StackLayout _ContentStackLayout = new StackLayout { Orientation = StackOrientation.Horizontal };
private readonly ContentView _ContentView = new ContentView
{
HorizontalOptions = LayoutOptions.FillAndExpand
};
private readonly StackLayout _ChildrenStackLayout = new StackLayout
{
Orientation = StackOrientation.Vertical,
Spacing = 0,
IsVisible = false
};
private IList<TreeViewNode> _Children = new ObservableCollection<TreeViewNode>();
private readonly TapGestureRecognizer _TapGestureRecognizer = new TapGestureRecognizer();
private readonly TapGestureRecognizer _ExpandButtonGestureRecognizer = new TapGestureRecognizer();
private readonly TapGestureRecognizer _DoubleClickGestureRecognizer = new TapGestureRecognizer();
#endregion
#region Internal Fields
internal readonly BoxView SelectionBoxView = new BoxView { Color = Color.Red, Opacity = .5, IsVisible = false };
#endregion
#region Private Properties
private TreeView ParentTreeView => Parent?.Parent as TreeView;
private double IndentWidth => Depth * SpacerWidth;
private int SpacerWidth { get; } = 30;
private int Depth => ParentTreeViewItem?.Depth + 1 ?? 0;
private bool _ShowExpandButtonIfEmpty = false;
private Color _SelectedBackgroundColor = Color.Blue;
private double _SelectedBackgroundOpacity = .3;
#endregion
#region Events
public event EventHandler Expanded;
/// <summary>
/// Occurs when the user double clicks on the node
/// </summary>
public event EventHandler DoubleClicked;
#endregion
#region Protected Overrides
protected override void OnParentSet()
{
base.OnParentSet();
Render();
}
#endregion
#region Public Properties
public string NodeKey { get; set; }
public int NodeId { get; set; }
public bool IsSelected
{
get => SelectionBoxView.IsVisible;
set => SelectionBoxView.IsVisible = value;
}
public bool IsExpanded
{
get => _ChildrenStackLayout.IsVisible;
set
{
_ChildrenStackLayout.IsVisible = value;
Render();
if (value)
{
Expanded?.Invoke(this, new EventArgs());
}
}
}
/// <summary>
/// set to true to show the expand button in case we need to poulate the child nodes on demand
/// </summary>
public bool ShowExpandButtonIfEmpty
{
get { return _ShowExpandButtonIfEmpty; }
set { _ShowExpandButtonIfEmpty = value; }
}
/// <summary>
/// set BackgroundColor when node is tapped/selected
/// </summary>
public Color SelectedBackgroundColor
{
get { return _SelectedBackgroundColor; }
set { _SelectedBackgroundColor = value; }
}
/// <summary>
/// SelectedBackgroundOpacity when node is tapped/selected
/// </summary>
public Double SelectedBackgroundOpacity
{
get { return _SelectedBackgroundOpacity; }
set { _SelectedBackgroundOpacity = value; }
}
/// <summary>
/// customize expand icon based on isExpanded property and or data
/// </summary>
public DataTemplate ExpandButtonTemplate
{
get { return _ExpandButtonTemplate; }
set { _ExpandButtonTemplate = value; }
}
public View Content
{
get => _ContentView.Content;
set => _ContentView.Content = value;
}
public IList<TreeViewNode> Children
{
get => _Children;
set
{
if (_Children is INotifyCollectionChanged notifyCollectionChanged)
{
notifyCollectionChanged.CollectionChanged -= ItemsSource_CollectionChanged;
}
_Children = value;
if (_Children is INotifyCollectionChanged notifyCollectionChanged2)
{
notifyCollectionChanged2.CollectionChanged += ItemsSource_CollectionChanged;
}
TreeView.RenderNodes(_Children, _ChildrenStackLayout, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset), this);
Render();
}
}
/// <summary>
/// TODO: Remove this. We should be able to get the ParentTreeViewNode by traversing up through the Visual Tree by 'Parent', but this not working for some reason.
/// </summary>
public TreeViewNode ParentTreeViewItem
{
get => _ParentTreeViewItem;
set
{
_ParentTreeViewItem = value;
Render();
}
}
#endregion
#region Constructor
/// <summary>
/// Constructs a new TreeViewItem
/// </summary>
public TreeViewNode()
{
var itemsSource = (ObservableCollection<TreeViewNode>)_Children;
itemsSource.CollectionChanged += ItemsSource_CollectionChanged;
_TapGestureRecognizer.Tapped += TapGestureRecognizer_Tapped;
GestureRecognizers.Add(_TapGestureRecognizer);
_MainGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
_MainGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
_MainGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
_MainGrid.Children.Add(SelectionBoxView);
_ContentView.GestureRecognizers.Add(_TapGestureRecognizer);
_ContentStackLayout.Children.Add(_SpacerBoxView);
_ContentStackLayout.Children.Add(_ExpandButtonContent);
_ContentStackLayout.Children.Add(_ContentView);
SetExpandButtonContent(_ExpandButtonTemplate);
_ExpandButtonGestureRecognizer.Tapped += ExpandButton_Tapped;
_ExpandButtonContent.GestureRecognizers.Add(_ExpandButtonGestureRecognizer);
_DoubleClickGestureRecognizer.NumberOfTapsRequired = 2;
_DoubleClickGestureRecognizer.Tapped += DoubleClick;
_ContentView.GestureRecognizers.Add(_DoubleClickGestureRecognizer);
_MainGrid.Children.Add(_ContentStackLayout);
_MainGrid.Children.Add(_ChildrenStackLayout, 0, 1);
base.Children.Add(_MainGrid);
HorizontalOptions = LayoutOptions.FillAndExpand;
VerticalOptions = LayoutOptions.Start;
Render();
}
void _DoubleClickGestureRecognizer_Tapped(object sender, EventArgs e)
{
}
#endregion
#region Private Methods
/// <summary>
/// TODO: This is a little stinky...
/// </summary>
private void ChildSelected(TreeViewNode child)
{
//Um? How does this work? The method here is a private method so how are we calling it?
ParentTreeViewItem?.ChildSelected(child);
ParentTreeView?.ChildSelected(child);
}
private void Render()
{
_SpacerBoxView.WidthRequest = IndentWidth;
if ((Children == null || Children.Count == 0) && !ShowExpandButtonIfEmpty)
{
SetExpandButtonContent(_ExpandButtonTemplate);
return;
}
SetExpandButtonContent(_ExpandButtonTemplate);
foreach (var item in Children)
{
item.Render();
}
}
/// <summary>
/// Use DataTemplae
/// </summary>
private void SetExpandButtonContent(DataTemplate expandButtonTemplate)
{
if (expandButtonTemplate != null)
{
_ExpandButtonContent.Content = (View)expandButtonTemplate.CreateContent();
}
else
{
_ExpandButtonContent.Content = (View)new ContentView { Content = _EmptyBox };
}
}
#endregion
#region Event Handlers
private void ExpandButton_Tapped(object sender, EventArgs e)
{
_ExpandButtonClickedTime = DateTime.Now;
IsExpanded = !IsExpanded;
}
private void TapGestureRecognizer_Tapped(object sender, EventArgs e)
{
//TODO: Hack. We don't want the node to become selected when we are clicking on the expanded button
// if (DateTime.Now - _ExpandButtonClickedTime > new TimeSpan(0, 0, 0, 0, 50))
// {
ChildSelected(this);
// }
}
private void DoubleClick(object sender, EventArgs e)
{
DoubleClicked?.Invoke(this, new EventArgs());
}
private void ItemsSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
TreeView.RenderNodes(_Children, _ChildrenStackLayout, e, this);
Render();
}
#endregion
}
}
and the TreeView control :
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using Xamarin.Forms;
namespace Involys.PraxisDemo.Controls.InCustomTreeView
{
public class TreeView : ScrollView
{
#region Fields
private static StackLayout _StackLayout = new StackLayout { Orientation = StackOrientation.Vertical };
//TODO: This initialises the list, but there is nothing listening to INotifyCollectionChanged so no nodes will get rendered
private static ObservableCollection<TreeViewNode> _RootNodes = new ObservableCollection<TreeViewNode>();
private TreeViewNode _SelectedItem;
#endregion
#region Public Properties
/// <summary>
/// The item that is selected in the tree
/// TODO: Make this two way - and maybe eventually a bindable property
/// </summary>
public static readonly BindableProperty RootNodesProperty =
BindableProperty.Create(nameof(RootNodes), typeof(IEnumerable<TreeViewNode>), typeof(TreeView), null,
BindingMode.Default, null, OnRootNodesChanged);
public static readonly BindableProperty SelectedItemProperty =
BindableProperty.Create(nameof(SelectedItem), typeof(TreeViewNode), typeof(TreeView), null,
BindingMode.TwoWay);
public IEnumerable<TreeViewNode> RootNodes
{
get
{
return (IEnumerable<TreeViewNode>)GetValue(RootNodesProperty);
}
set
{
SetValue(RootNodesProperty, value);
}
}
public TreeViewNode SelectedItem
{
get { return (TreeViewNode)GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
#endregion
#region Events
/// <summary>
/// Occurs when the user selects a TreeViewItem
/// </summary>
public event EventHandler SelectedItemChanged;
#endregion
#region Constructor
public TreeView()
{
_StackLayout = new StackLayout { Orientation = StackOrientation.Vertical };
Content = _StackLayout;
}
#endregion
//selection
#region Internal Methods
/// <summary>
/// TODO: A bit stinky but better than bubbling an event up...
/// </summary>
internal void ChildSelected(TreeViewNode child)
{
SelectedItem = child;
child.IsSelected = true;
child.SelectionBoxView.Color = child.SelectedBackgroundColor;
child.SelectionBoxView.Opacity = child.SelectedBackgroundOpacity;
RemoveSelectionRecursive(RootNodes);
}
#endregion
#region Private Methods
private void RemoveSelectionRecursive(IEnumerable<TreeViewNode> nodes)
{
foreach (var treeViewItem in nodes)
{
if (treeViewItem != SelectedItem)
{
treeViewItem.IsSelected = false;
}
RemoveSelectionRecursive(treeViewItem.Children);
}
}
#endregion
//end selection
#region Internal Static Methods
internal static void RenderNodes(IEnumerable<TreeViewNode> childTreeViewItems, StackLayout parent, NotifyCollectionChangedEventArgs e, TreeViewNode parentTreeViewItem)
{
if (e.Action != NotifyCollectionChangedAction.Add)
{
//TODO: Reintate this...
parent.Children.Clear();
AddItems(childTreeViewItems, parent, parentTreeViewItem);
}
else
{
AddItems(e.NewItems.Cast<TreeViewNode>(), parent, parentTreeViewItem);
}
}
static void OnRootNodesChanged(BindableObject bindable, object oldvalue, object newvalue)
{
System.Diagnostics.Debug.WriteLine("source changed");
if (oldvalue is INotifyCollectionChanged notifyCollectionChanged)
{
notifyCollectionChanged.CollectionChanged += (s, e) =>
{
RenderNodes((IEnumerable<TreeViewNode>)newvalue, _StackLayout, e, null);
};
}
RenderNodes((IEnumerable<TreeViewNode>)newvalue, _StackLayout, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset), null);
}
#endregion
#region Private Static Methods
private static void AddItems(IEnumerable<TreeViewNode> childTreeViewItems, StackLayout parent, TreeViewNode parentTreeViewItem)
{
foreach (var childTreeNode in childTreeViewItems)
{
if (!parent.Children.Contains(childTreeNode))
{
parent.Children.Add(childTreeNode);
}
childTreeNode.ParentTreeViewItem = parentTreeViewItem;
}
}
#endregion
}
}
so in my viewmodel I create my TreeViewNode and bind it to RootNodes property of TreeView. so I added :
private ObservableCollection<TreeViewNode> _treeviewSource;
public ObservableCollection<TreeViewNode> TreeviewSource
{
get => _treeviewSource;
set
{
_treeviewSource = value;
RaisePropertyChanged(() => TreeviewSource);
}
}
But raising property changed while having a big list of TreeViewNode takes waaay too much time. And that is my problem ... I suppose the problem is that it notifies the UI foreach TreeViewNode(stacklayout) in the list and that what it takes time.
I Hope you guys can help with that. Thanks

Removing back swipe gesture from page using xamarin forms

Is there a way i can disable the back swipe to previous page option for iOS on one single page of my project ?
You can achieve this by implementing a custom renderer and setting the right property for this. You can see a sample implementation underneath. The right property, in this case, is InteractivePopGestureRecognizer which you need to set to false.
Do this in the ViewWillAppear so the NavigationController is initialized.
using DisableSwipe.iOS;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(ContentPage), typeof(NoBackSwipeRenderer))]
namespace DisableSwipe.iOS
{
public class NoBackSwipeRenderer : PageRenderer
{
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
if (ViewController?.NavigationController != null)
ViewController.NavigationController.InteractivePopGestureRecognizer.Enabled = false;
}
}
}
#Symorp
You could do it like so:
public class YourCustomPageRenderer : PageRenderer
{
private YourCustomPage _yourCustomPage;
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
_yourCustomPage = e.NewElement as YourCustomPage;
if (_yourCustomPage != null)
{
_yourCustomPage.PropertyChanged += YourCustomPagePropertyChangedEventHandler;
}
}
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
SetInteractivePopGestureRecognizerEnabled(isEnabled: false);
}
private void YourCustomPagePropertyChangedEventHandler(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
{
if (propertyChangedEventArgs.PropertyName == nameof(YourCustomPage.IsInteractivePopGestureRecognizerEnabled))
{
SetInteractivePopGestureRecognizerEnabled(_yourCustomPage.IsInteractivePopGestureRecognizerEnabled);
}
}
private void SetInteractivePopGestureRecognizerEnabled(bool isEnabled)
{
var interactivePopGestureRecognizer = ViewController?.NavigationController?.InteractivePopGestureRecognizer;
if (interactivePopGestureRecognizer != null)
{
//Prevents the back-swipe-gesture when the user wants to swipe a page away (from left edge of the screen)
interactivePopGestureRecognizer.Enabled = isEnabled;
}
}
}
public class YourCustomPage : ContentPage
{
/// <summary>
/// If you need it as bindable property, feel free to create a <see cref="BindableProperty"/>.
/// </summary>
public bool IsInteractivePopGestureRecognizerEnabled { get; set; }
}
Feel free to adjust to your needs! :-)
I omitted the export renderer attribute etc., just for simplicity.

Xamarin Forms Map Tap Gesture IOS

I implement a custom renderer for Xamarin Forms Map to implement a Tap Event.
I the PCL I have this code :
public class ExtMap : Map
{
/// <summary>
/// Event thrown when the user taps on the map
/// </summary>
public event EventHandler<MapTapEventArgs> Tapped;
#region Constructors
/// <summary>
/// Default constructor
/// </summary>
public ExtMap()
{
}
/// <summary>
/// Constructor that takes a region
/// </summary>
/// <param name="region"></param>
public ExtMap(MapSpan region)
: base(region)
{
}
#endregion
public void OnTap(Position coordinate)
{
OnTap(new MapTapEventArgs { Position = coordinate });
}
protected virtual void OnTap(MapTapEventArgs e)
{
var handler = Tapped;
if (handler != null)
handler(this, e);
}
}
And in my IOS Project this code :
public class ExtMapRenderer : MapRenderer
{
private readonly UITapGestureRecognizer _tapRecogniser;
public ExtMapRenderer()
{
_tapRecogniser = new UITapGestureRecognizer(OnTap)
{
NumberOfTapsRequired = 1,
NumberOfTouchesRequired = 1
};
}
private void OnTap(UITapGestureRecognizer recognizer)
{
var cgPoint = recognizer.LocationInView(Control);
var location = ((MKMapView)Control).ConvertPoint(cgPoint, Control);
((ExtMap)Element).OnTap(new Position(location.Latitude, location.Longitude));
}
protected override void OnElementChanged(ElementChangedEventArgs<View> e)
{
if (Control != null)
Control.RemoveGestureRecognizer(_tapRecogniser);
base.OnElementChanged(e);
if (Control != null)
{
var nativeMap = Control as MKMapView;
nativeMap.ShowsUserLocation = true;
Control.AddGestureRecognizer(_tapRecogniser);
}
}
}
In the simulator sometimes the event is raised, but I have to click randomly a lot of time in the map.
In my iPhone, the event is never raised.
In Android phone & Emulator, the event is working correctly so I suspect a bad implementation in IOS Project but I do not know how I can improve it.
I have an iOS map renderer where tap is working correctly.
My OnElementChanged is a bit different to yours:
private MKMapView Map => Control as MKMapView;
protected override void OnElementChanged(ElementChangedEventArgs<View> e)
{
base.OnElementChanged(e);
if (e.OldElement != null && Map != null)
{
Control?.RemoveGestureRecognizer(_tapRecogniser);
}
if (e.NewElement != null)
{
Control?.AddGestureRecognizer(_tapRecogniser);
}
}

How to create command menu item with checkbox?

I'm writing a VSPackage and I need to have menu item with checkbox, just like on this sample image below:
I went through this msdn reference regarding .vsct files, bud didn't fine any information explaining how to do it. What I have now is standard menu item with icon and text (code sample from MyPackage.vsct file):
<Buttons>
<Button guid="guidMyPackageCmdSet" id="cmdidMyPackage" type="Button">
<Icon guid="guidImages" id="myPackageBitmap" />
<CommandFlag>TextChanges</CommandFlag>
<CommandFlag>DontCache</CommandFlag>
<CommandFlag>FixMenuController</CommandFlag>
<Strings>
<ButtonText>MyPackage</ButtonText>
</Strings>
</Button>
</Buttons>
I need this additional checkbox. How to do it?
The properties like Checked, Visible, Enabled or Supported can´t be defined via the VSCT file. You need a command handler that controls the command´s state. I´ve created a base class that wraps the creation of the OleMenuCommand instance and handles the command´s BeforeQueryStatus event. This is a slimmed version of my implementation, but it will give you an idea how to solve it...
internal abstract class CommandHandler : IDisposable
{
private readonly OleMenuCommand command;
protected CommandHandler(Guid group, int id)
{
var commandid = CommandID(group, id);
this.command = new OleMenuCommand(this.Invoke, commandId);
this.command.BeforeQueryStatus += this.OnBeforeQueryStatus;
}
protected virtual void OnExecute() { }
protected virtual void OnQueryStatus(QueryStatusEventArgs e) { }
private void Invoke(object sender, EventArgs e)
{
this.OnExecute();
}
private void OnBeforeQueryStatus(object sender, EventArgs e)
{
OleMenuCommand command;
if ((command = sender as OleMenuCommand) != null)
{
var e = new QueryCommandEventArgs
{
Checked = command.Checked,
}
this.OnQueryStatus(e);
command.Checked = e.Checked;
}
}
public void Dispose()
{
this.command.BeforeQueryStatus -= this.OnBeforeQueryStatus;
}
}
public class QueryCommandEventArgs : EventArgs
{
public bool Checked { get; set; }
}
The CommandHandler class allows to control the state of any menu command. Just derive new handler implementations from it and override the OnExecute and OnQueryStatus methods, like...
internal sealed class MyCommand : CommandHandler
{
private bool checked;
public MyCommand() : base(GuidCmdSet, MyCommandId) { }
protected override void OnExecute()
{
this.checked = !this.checked; // toggle checked state
}
protected override void OnQueryStatus(QueryStatusEventArgs e)
{
e.Checked = this.checked;
}
}

Prism2 Region Adapter for AvalonDock's DocumentPane and DockingPane?

Does anyone have sample code on how to create a Region Adapter for AvalonDock's DocumentPane and DockingPane?
The Markus Raufer has added two region adapters to the CompositeWpfContrib project at CodePlex that supports both DocumentPane and DockingPane.
I have used Raffaeu Bermuda snippets to support Avalon tab region adapter, but found that there is some issues are not solved:
1- It does not support Activating a a certain view (aka - tab - DockableContent), so the code Region.Activate(object view) will not work.
2- All the Tabs are active by default in the region. So Region.ActiveViews collection by default has all the views, this is not ideal, as sometimes I needed to verify if a view is active or not (you could imagine a save button on a tool bar region that executes a SaveCommand only on the current active view = tab in our case)
3- Closed views doesn't actually get closed, only hidden. Even if you set the HideOnClose = true when adding the newDockableContent, it is still not removed from Region.Views collection. This could lead to memory leaks issues.
4- If you have a previously added DockableContent in the Pane, they will not get synchronized and added to the Region.Views collection.
So here are the code I am using now, it is just a small tweak from the Selector Adapter and Selector Sync Behavior found in PRISM source code:
AvalonRegionAdapter Class:
public class AvalonRegionAdapter : RegionAdapterBase<DocumentPane>
{
public AvalonRegionAdapter(IRegionBehaviorFactory factory) : base(factory) {}
protected override void AttachBehaviors(IRegion region, DocumentPane regionTarget)
{
if (region == null) throw new System.ArgumentNullException("region");
//Add the behavior that syncs the items source items with the rest of the items
region.Behaviors.Add(AvalonDocumentSyncBehavior.BehaviorKey,
new AvalonDocumentSyncBehavior()
{
HostControl = regionTarget
});
base.AttachBehaviors(region, regionTarget);
}
protected override void Adapt(IRegion region, DocumentPane regionTarget){ }
protected override IRegion CreateRegion()
{
return new Region();
}
}
AvalonDocumentSyncBehavior Behavior Code:
public class AvalonDocumentSyncBehavior : RegionBehavior, IHostAwareRegionBehavior
{
/// <summary>
/// Name that identifies the SelectorItemsSourceSyncBehavior behavior in a collection of RegionsBehaviors.
/// </summary>
public static readonly string BehaviorKey = "AvalonDocumentSyncBehavior";
private bool updatingActiveViewsInHostControlSelectionChanged;
private Selector hostControl;
/// <summary>
/// Gets or sets the <see cref="DependencyObject"/> that the <see cref="IRegion"/> is attached to.
/// </summary>
/// <value>
/// A <see cref="DependencyObject"/> that the <see cref="IRegion"/> is attached to.
/// </value>
/// <remarks>For this behavior, the host control must always be a <see cref="Selector"/> or an inherited class.</remarks>
public DependencyObject HostControl
{
get
{
return this.hostControl;
}
set
{
this.hostControl = value as Selector;
}
}
/// <summary>
/// Starts to monitor the <see cref="IRegion"/> to keep it in synch with the items of the <see cref="HostControl"/>.
/// </summary>
protected override void OnAttach()
{
bool itemsSourceIsSet = this.hostControl.ItemsSource != null;
if (itemsSourceIsSet)
{
//throw new InvalidOperationException(Resources.ItemsControlHasItemsSourceException);
}
this.SynchronizeItems();
this.hostControl.SelectionChanged += this.HostControlSelectionChanged;
this.Region.ActiveViews.CollectionChanged += this.ActiveViews_CollectionChanged;
this.Region.Views.CollectionChanged += this.Views_CollectionChanged;
}
private void Views_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
int startIndex = e.NewStartingIndex;
foreach (object newItem in e.NewItems)
{
UIElement view = newItem as UIElement;
TabViewModel viewModel = ((UserControl)view).DataContext as TabViewModel;
if (view != null)
{
DockableContent newDockableContent = new DockableContent();
newDockableContent.Content = newItem;
//if associated view has metadata then apply it.
newDockableContent.Title = view.GetType().ToString();
if (viewModel != null)
{
//Image img = new Image();
//img.Source = new BitmapImage(new Uri(#"Resources/Alerts.png", UriKind.Relative));
newDockableContent.Title = viewModel.TabModel.Title;
newDockableContent.IsCloseable = viewModel.TabModel.CanClose;
//newContentPane.Icon = img.Source;
}
//When contentPane is closed remove from the associated region
newDockableContent.Closed += new EventHandler(newDockableContent_Closed);
newDockableContent.HideOnClose = false;
this.hostControl.Items.Add(newDockableContent);
newDockableContent.Activate();
}
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (object oldItem in e.OldItems)
{
this.hostControl.Items.Remove(oldItem);
}
}
}
void newDockableContent_Closed(object sender, EventArgs e)
{
var dockableContent = sender as DockableContent;
if(dockableContent != null)
if (this.Region.Views.Contains(dockableContent.Content))
{
this.Region.Remove(dockableContent.Content);
}
}
private void SynchronizeItems()
{
List<object> existingItems = new List<object>();
// Control must be empty before "Binding" to a region
foreach (object childItem in this.hostControl.Items)
{
existingItems.Add(childItem);
}
foreach (object view in this.Region.Views)
{
this.hostControl.Items.Add(view);
}
foreach (object existingItem in existingItems)
{
this.Region.Add(existingItem);
}
}
private void ActiveViews_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (this.updatingActiveViewsInHostControlSelectionChanged)
{
// If we are updating the ActiveViews collection in the HostControlSelectionChanged, that
// means the user has set the SelectedItem or SelectedItems himself and we don't need to do that here now
return;
}
if (e.Action == NotifyCollectionChangedAction.Add)
{
var selectedDockableContent = this.hostControl.SelectedItem as DockableContent;
if (selectedDockableContent != null
&& selectedDockableContent.Content != null
&& selectedDockableContent.Content != e.NewItems[0]
&& this.Region.ActiveViews.Contains(selectedDockableContent.Content))
{
this.Region.Deactivate(selectedDockableContent.Content);
}
var _UIElement = e.NewItems[0] as FrameworkElement;
this.hostControl.SelectedItem = _UIElement.Parent;
}
else if (e.Action == NotifyCollectionChangedAction.Remove &&
e.OldItems.Contains(this.hostControl.SelectedItem))
{
this.hostControl.SelectedItem = null;
}
}
private void HostControlSelectionChanged(object sender, SelectionChangedEventArgs e)
{
try
{
// Record the fact that we are now updating active views in the HostControlSelectionChanged method.
// This is needed to prevent the ActiveViews_CollectionChanged() method from firing.
this.updatingActiveViewsInHostControlSelectionChanged = true;
object source;
source = e.OriginalSource;
if (source == sender)
{
foreach (object item in e.RemovedItems)
{
// check if the view is in both Views and ActiveViews collections (there may be out of sync)
var dockableContent = item as DockableContent;
if (this.Region.Views.Contains(dockableContent.Content) && this.Region.ActiveViews.Contains(dockableContent.Content))
{
this.Region.Deactivate(dockableContent.Content);
}
}
foreach (object item in e.AddedItems)
{
var dockableContent = item as DockableContent;
if (this.Region.Views.Contains(dockableContent.Content) &&
!this.Region.ActiveViews.Contains(dockableContent.Content))
{
this.Region.Activate(dockableContent.Content);
}
}
}
}
finally
{
this.updatingActiveViewsInHostControlSelectionChanged = false;
}
}
}
Code on bootstrapper to configure the Adapter
protected override RegionAdapterMappings ConfigureRegionAdapterMappings()
{
var mappings = base.ConfigureRegionAdapterMappings();
mappings.RegisterMapping(typeof(AvalonDock.DocumentPane),
this.Container.Resolve<AvalonRegionAdapter>());
return mappings;
}
Then you need the TabModel and the TabViewModel as fromRaffaeu Bermuda
public sealed class TabModel : DependencyObject
{
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 DependencyProperty TitleProperty =
DependencyProperty.Register("Title", typeof(string), typeof(TabModel));
public bool CanClose
{
get { return (bool)GetValue(CanCloseProperty); }
set { SetValue(CanCloseProperty, value); }
}
// Using a DependencyProperty as the backing store for CanClose. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CanCloseProperty =
DependencyProperty.Register("CanClose", typeof(bool), typeof(TabModel));
public bool IsModified
{
get { return (bool)GetValue(IsModifiedProperty); }
set { SetValue(IsModifiedProperty, value); }
}
// Using a DependencyProperty as the backing store for IsModified. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsModifiedProperty =
DependencyProperty.Register("IsModified", typeof(bool), typeof(TabModel));
}
And a TabViewModel acting as a base class:
public class TabViewModel : INotifyPropertyChanged
{
private TabModel _tabModel;
public TabModel TabModel
{
get { return this._tabModel; }
set
{
this._tabModel = value;
OnPropertyChanged("TabModel");
}
}
public TabViewModel()
{
this.TabModel = new TabModel();
this.TabModel.CanClose = true;
this.TabModel.IsModified = false;
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
Let me know if you need further help, I will post a blog this in the near future.
Since the Avalon DocumentPane and DockingPane are both based on the System.Windows.Controls.Primitives.Selector you can use the default SelectorRegionAdapter in Prism.
Just base your control on DockableContent
<ad:DockableContent x:Class="DesignerWPF.UserControl1"
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"
mc:Ignorable="d"
xmlns:ad="clr-namespace:AvalonDock;assembly=AvalonDock"
d:DesignHeight="300" d:DesignWidth="300" Title="dans">
<Grid>
<TextBox Text="sdfdf"></TextBox>
</Grid>
</ad:DockableContent>
on your main Shell.xmal set the regions in the dockablepane
<ad:DockingManager x:Name="dockManager" Grid.Row="1" Margin="0,4,0,0">
<ad:ResizingPanel Orientation="Horizontal">
<ad:DockablePane cal:RegionManager.RegionName="LeftRegion">
</ad:DockablePane>
<ad:DocumentPane cal:RegionManager.RegionName="DocumentRegion">
</ad:DocumentPane>
</ad:ResizingPanel>
</ad:DockingManager>
then when you initialize your presenter for your control it will get displayed in the dock.
public class UserTestControl : IModule
{
public UserTestControl(IUnityContainer container, IRegionManager regionManager)
{
Container = container;
RegionManager = regionManager;
}
public void Initialize()
{
var addFundView = Container.Resolve<UserControl1>();
RegionManager.Regions["LeftRegion"].Add(addFundView);
}
public IUnityContainer Container { get; private set; }
public IRegionManager RegionManager { get; private set; }
}
My advice would be to look in Microsoft.Practices.Composite.Presentation.Regions in the Prism source. Specifically, take a look at the ItemsControlRegionAdapter and use it as a template. Remember to inherit from RegionAdapterBase<>:
public class ItemsControlRegionAdapter : RegionAdapterBase<ItemsControl>
and to override ConfigureRegionAdapterMappings() in the bootstrapper. That would look something like:
protected override RegionAdapterMappings ConfigureRegionAdapterMappings()
{
RegionAdapterMappings mappings = base.ConfigureRegionAdapterMappings();
mappings.RegisterMapping(typeof(Canvas), Container.Resolve<CanvasRegionAdapter>());
return mappings;
}

Resources