I am creating a kiosk–like application for a Windows 10 PC with a touch screen as its only input interface. What I want to remove is the white touch dot that is displayed as a visual touch feedback (mostly together with a circle, which can be turned off).
Does anyone know how to do this?
I have already searched the registry if there was a cursor (*.cur) file which is used but did not find any results. Due to that I guess that the touch feedback is displayed differently.
Just to make sure — I do not want to lose touch functionality, only the visual feedback needs to be gone.
For WPF programs:
In order to remove the touch feedback from WPF elements, it's enough to set Stylus.IsTapFeedbackEnabled dependency property to false. You may want to do that within styles:
<Style x:Key="yourStyle">
<Setter Property="Stylus.IsTapFeedbackEnabled" Value="False" />
</Style>
The white touch dot you cited is actually a cursor. Whenever you touch the screen, Windows replaces the cursor set for the control with the touch cursor. To avoid this honestly ugly behavior, you can set your cursor to Cursors.None, so that the cursor will be hidden. Paste the code below inside your Window and you won't see your white touch dot anymore.
protected override void OnPreviewTouchDown(TouchEventArgs e)
{
base.OnPreviewTouchDown(e);
Cursor = Cursors.None;
}
protected override void OnPreviewTouchMove(TouchEventArgs e)
{
base.OnPreviewTouchMove(e);
Cursor = Cursors.None;
}
protected override void OnGotMouseCapture(MouseEventArgs e)
{
base.OnGotMouseCapture(e);
Cursor = Cursors.Arrow;
}
protected override void OnPreviewMouseMove(MouseEventArgs e)
{
base.OnPreviewMouseMove(e);
if (e.StylusDevice == null)
Cursor = Cursors.Arrow;
}
Your cursor will always be set to Cursors.Arrow when you're using the mouse, though, so you could lose a different cursor you had set to the window. It'd be an easy task to tweak this behavior — my code only serves for demonstration purposes.
For Windows Forms programs:
Looking at the Reference Source for Stylus.IsTapFeedbackEnabled, I've figured out the P/Invoke calls that happen under the hood. Here's what I've come up with for Windows Form applications:
using System;
using System.ComponentModel;
using System.Windows.Forms;
namespace TouchableWinform
{
public class TouchableForm : Form
{
private const string TOUCH_SUPPORT_CATEGORY = "Touch support";
private const bool IS_PRESS_AND_HOLD_ENABLED_DEFAULT_VALUE = false;
private const bool IS_FLICKS_ENABLED_DEFAULT_VALUE = false;
private const bool IS_TAP_FEEDBACK_DEFAULT_VALUE = false;
private const bool IS_TOUCH_FEEDBACK_DEFAULT_VALUE = false;
/// <summary>
/// Gets or sets a values indicating whether press and hold is enabled.
/// </summary>
[Category(TOUCH_SUPPORT_CATEGORY), Description("Gets or sets a values indicating whether press and hold is enabled."), EditorBrowsable(EditorBrowsableState.Always), DefaultValue(IS_PRESS_AND_HOLD_ENABLED_DEFAULT_VALUE)]
public bool IsPressAndHoldEnabled
{
get;
set;
} = IS_PRESS_AND_HOLD_ENABLED_DEFAULT_VALUE;
/// <summary>
/// Gets or sets a value indicating whether flicks are enabled.
/// </summary>
[Category(TOUCH_SUPPORT_CATEGORY), Description("Gets or sets a value indicating whether flicks are enabled."), EditorBrowsable(EditorBrowsableState.Always), DefaultValue(IS_FLICKS_ENABLED_DEFAULT_VALUE)]
public bool IsFlicksEnabled
{
get;
set;
} = IS_FLICKS_ENABLED_DEFAULT_VALUE;
/// <summary>
/// Gets or sets whether a value indicating whether tap feedback is enabled.
/// </summary>
[Category(TOUCH_SUPPORT_CATEGORY), Description("Gets or sets whether a value indicating whether tap feedback is enabled."), EditorBrowsable(EditorBrowsableState.Always), DefaultValue(IS_TAP_FEEDBACK_DEFAULT_VALUE)]
public bool IsTapFeedbackEnabled
{
get;
set;
} = IS_TAP_FEEDBACK_DEFAULT_VALUE;
/// <summary>
/// Gets or sets whether a value indicating whether touch feedback is enabled.
/// </summary>
[Category(TOUCH_SUPPORT_CATEGORY), Description("Gets or sets whether a value indicating whether touch feedback is enabled."), EditorBrowsable(EditorBrowsableState.Always), DefaultValue(IS_TOUCH_FEEDBACK_DEFAULT_VALUE)]
public bool IsTouchFeedbackEnabled
{
get;
set;
} = IS_TOUCH_FEEDBACK_DEFAULT_VALUE;
/// <summary>
/// Processes Windows messages.
/// </summary>
/// <param name="m">The Windows <see cref="Message"/> to process.</param>
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case 0x2CB:
m.Result = new IntPtr(1);
break;
case 0x2CC:
uint flags = 0;
if (!IsPressAndHoldEnabled)
flags |= 1;
if (!IsTapFeedbackEnabled)
flags |= 8;
flags |= IsTouchFeedbackEnabled ? (uint)0x100 : 0x200;
if (!IsFlicksEnabled)
flags |= 0x10000;
m.Result = new IntPtr(flags);
break;
default:
base.WndProc(ref m);
break;
}
}
}
}
Unfortunately, for some reason I couldn't still figure out, it doesn't quite look to be working. I'm open to any suggestions.
In Windows 10, you can use pointer messages to handle touches. This would make WPF act like native UWP app behavior. The white dot will disappear and touch move animation get fixed.
If you are using .NET Core, add this line to your App.xaml.cs constructor:
public partial class App : Application
{
public App()
{
AppContext.SetSwitch("Switch.System.Windows.Input.Stylus.EnablePointerSupport", true);
}
}
For .NET Framework, you should add App.config file to project. The contents are shown below:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7" />
</startup>
<runtime>
<AppContextSwitchOverrides value="Switch.System.Windows.Input.Stylus.EnablePointerSupport=true" />
</runtime>
</configuration>
Sources:
How to enable pointer message support in WPF .NET Core
WPF will have a touch offset after trun on the WM_Pointer message
Related
I have a similar question to this, however for SpherePointer.
Using MRTK 2.2 as acquired by NuGet for Unity, I'm getting this warning pretty much every frame:
Maximum number of 64 colliders found in SpherePointer overlap query. Consider increasing the query buffer size in the pointer profile.
UnityEngine.Debug:LogWarning(Object)
Microsoft.MixedReality.Toolkit.Input.SpherePointerQueryInfo:TryUpdateQueryBufferForLayerMask(LayerMask, Vector3, QueryTriggerInteraction)
Microsoft.MixedReality.Toolkit.Input.SpherePointer:OnPreSceneQuery()
Microsoft.MixedReality.Toolkit.Input.FocusProvider:UpdatePointer(PointerData)
Microsoft.MixedReality.Toolkit.Input.FocusProvider:UpdatePointers()
Microsoft.MixedReality.Toolkit.Input.FocusProvider:Update()
Microsoft.MixedReality.Toolkit.<>c:<UpdateAllServices>b__63_0(IMixedRealityService)
Microsoft.MixedReality.Toolkit.MixedRealityToolkit:ExecuteOnAllServices(IEnumerable`1, Action`1)
Microsoft.MixedReality.Toolkit.MixedRealityToolkit:ExecuteOnAllServicesInOrder(Action`1)
Microsoft.MixedReality.Toolkit.MixedRealityToolkit:UpdateAllServices()
Microsoft.MixedReality.Toolkit.MixedRealityToolkit:Update()
I was able to successfully remove the similar warning for PokePointer using #Julia's response but I'm stumped as to how to do it for the GrabPointer prefab.
This prefab has a SpherePointer script attached but the SceneQueryBufferSize property isn't exposed in the inspector because SpherePointer's custom inspector (ShperePointerInspector.cs) doesn't expose it.
Great question! It looks like you've actually found a bug. Please use the following code to work around this for now, we will post a fix shortly. The issue to track this is here: issue 6878
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
using Microsoft.MixedReality.Toolkit.Input.Editor;
using Microsoft.MixedReality.Toolkit.Input;
using UnityEditor;
namespace Microsoft.MixedReality.Toolkit.Input
{
[CustomEditor(typeof(SpherePointer))]
public class SpherePointerInspector : BaseControllerPointerInspector
{
private SerializedProperty sphereCastRadius;
private SerializedProperty nearObjectMargin;
private SerializedProperty grabLayerMasks;
private SerializedProperty triggerInteraction;
private SerializedProperty sceneQueryBufferSize;
private bool spherePointerFoldout = true;
protected override void OnEnable()
{
base.OnEnable();
sphereCastRadius = serializedObject.FindProperty("sphereCastRadius");
sceneQueryBufferSize = serializedObject.FindProperty("sceneQueryBufferSize");
nearObjectMargin = serializedObject.FindProperty("nearObjectMargin");
grabLayerMasks = serializedObject.FindProperty("grabLayerMasks");
triggerInteraction = serializedObject.FindProperty("triggerInteraction");
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
serializedObject.Update();
spherePointerFoldout = EditorGUILayout.Foldout(spherePointerFoldout, "Sphere Pointer Settings", true);
if (spherePointerFoldout)
{
using (new EditorGUI.IndentLevelScope())
{
EditorGUILayout.PropertyField(sphereCastRadius);
EditorGUILayout.PropertyField(sceneQueryBufferSize);
EditorGUILayout.PropertyField(nearObjectMargin);
EditorGUILayout.PropertyField(triggerInteraction);
EditorGUILayout.PropertyField(grabLayerMasks, true);
}
}
serializedObject.ApplyModifiedProperties();
}
}
}
Thanks Julia,
Since I can't modify SpherePointerInspector.cs in my project as the MRTK is obtained from NuGet and has no actual .cs files to edit. Here's the solution I came up with as a temporary workaround until MRTK 2.3 is released.
I replaced SpherePointer.cs with MySpherePointer.cs on a copy of the GrabPointer.prefab.
MySpherePointer.cs
using Microsoft.MixedReality.Toolkit.Input;
/// <summary>
/// This class exists soley to expose SpherePointer.queryBufferPointerSize which is
/// masked by the default SpherePointerInspector.
/// </summary>
public class MySpherePointer : SpherePointer
{
}
MySpherePointerInspector.cs
using Microsoft.MixedReality.Toolkit.Input;
using UnityEditor;
/// <summary>
/// This class exists soley to expose SpherePointer.queryBufferPointerSize which is
/// masked by the default SpherePointerInspector.
/// </summary>
[CustomEditor(typeof(MySpherePointer))]
public class MySpherePointerInspector : SpherePointerInspector
{
private SerializedProperty sceneQueryBufferSize;
private bool hiddenSpherePointerFoldout = true;
protected override void OnEnable()
{
base.OnEnable();
sceneQueryBufferSize = serializedObject.FindProperty("sceneQueryBufferSize");
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
hiddenSpherePointerFoldout = EditorGUILayout.Foldout(hiddenSpherePointerFoldout, "Hidden Sphere Pointer Settings", true);
if (hiddenSpherePointerFoldout)
{
using (new EditorGUI.IndentLevelScope())
{
EditorGUILayout.PropertyField(sceneQueryBufferSize);
}
}
serializedObject.ApplyModifiedProperties();
}
}
I tried using the slider component for the first time and immediately some question came up:
How do I make the slider snap to the positions of the TickMarks?
If that's not possible, what are they for?
How do I change the amount of marks or remove all of them if I want to slider without stepping?
Why, when I change the Slider Track settings the visuals do not adjust?
I already see this going into a feature request ;)
These questions are probably best filed as issues on the MRTK github page. here's a link to file an issue.
But, answering your questions here for future visitors:
How do I make the slider snap to the positions of the TickMarks?
The default PinchSlider does not provide that functionality, but here is an example of how to do this by extending pinchslider (reference: issue 4140
public class SliderWithSnapPoints : PinchSlider
{
[SerializeField]
[Tooltip("The number of snap points")]
float snapPoints = 100;
float lastSnapPoint;
float snapPointSize;
public override void OnPointerDown(MixedRealityPointerEventData eventData)
{
base.OnPointerDown(eventData);
if (eventData.used)
{
lastSnapPoint = SliderValue;
snapPointSize = 1f / snapPoints;
}
}
/// <summary>
/// Handle slider value changes by dragging, and commit these changes.
/// </summary>
/// <remarks>
/// Note, this requires the MRTK pinch slider to implement this function signature,
/// and the pinch slider needs to call this function instead of instead setting SliderValue
/// directly.
/// </remarks>
protected override void OnPointerDragged(float newSliderValue)
{
var valueChange = Mathf.Abs(lastSnapPoint - newSliderValue);
if (valueChange >= snapPointSize)
{
lastSnapPoint = SliderValue = newSliderValue;
}
}
}
How do I change the amount of marks or remove all of them if I want to slider without stepping?
You can just delete or disable the tick marks in the scene hierarchy.
Why, when I change the Slider Track settings the visuals do not adjust?
The visuals do not adjust because they are not tied to the number of snap points, they are just provided for visual guidance.
I modified this implementation slightly:
using Microsoft.MixedReality.Toolkit.UI;
using UnityEngine;
public class SliderWithSnapPoints : PinchSlider
{
#pragma warning disable 649
[Tooltip("The number of snap points")]
[Min(2)]
[SerializeField] private int snapPoints = 5;
#pragma warning restore 649
/// <summary>
/// Handle slider value changes by dragging, and commit snap point aligned changes.
/// </summary>
/// <remarks>
/// Note, this requires the MRTK pinch slider to implement this function signature,
/// and the pinch slider needs to call this function instead of setting SliderValue
/// directly.
/// </remarks>
protected override void OnPointerDragged(float newSliderValue)
{
float gap = 1f / (snapPoints - 1f);
newSliderValue = 0.5f * gap + newSliderValue;
newSliderValue -= (newSliderValue % gap);
SliderValue = newSliderValue;
}
}
As the comment suggests you also have to edit the main PinchSlider script from:
public void OnPointerDragged(MixedRealityPointerEventData eventData)
{
if (eventData.Pointer == activePointer && !eventData.used)
{
var delta = activePointer.Position - startPointerPosition;
var handDelta = Vector3.Dot(SliderTrackDirection.normalized, delta);
SliderValue = Mathf.Clamp(startSliderValue + handDelta / SliderTrackDirection.magnitude, 0, 1);
// Mark the pointer data as used to prevent other behaviors from handling input events
eventData.Use();
}
}
To:
public void OnPointerDragged(MixedRealityPointerEventData eventData)
{
if (eventData.Pointer == activePointer && !eventData.used)
{
var delta = activePointer.Position - startPointerPosition;
var handDelta = Vector3.Dot(SliderTrackDirection.normalized, delta);
OnPointerDragged(Mathf.Clamp(startSliderValue + handDelta / SliderTrackDirection.magnitude, 0, 1));
// Mark the pointer data as used to prevent other behaviors from handling input events
eventData.Use();
}
}
protected virtual void OnPointerDragged(float newSliderValue)
{
SliderValue = newSliderValue;
}
I am having some issues with bindings using xamarin forms IOS.
I have bindings That display a loading label in the center of the screen when the first page appears.
After The listview loads the data the Loading label is hidden through bindings and the list view is displayed.
In the android project I have in this repo Xamarin forms IOS Android Test
It works fine. In the ios in the repro project the loading label is hidden when the application first loads and will display if you tap on the screen.
If you continue the loading label disappears and you see a blank screen but if you tap on the screen again the list data view appears.
I am thinking this is a bug with xamarin but I am hoping that my bindings are just incorrect. I work around for this issue would be appreciated.
I also two other problems. When you click on a list view item and the navigate back the data is not refreshed because onappearing is not triggered.
In android it is triggered without an issue. Sometimes onappearing does trigger after you tap on the screen like the steps mentioned above.
The last issue is that when switching between tabs IOS does not trigger on appearing. With android it does.
Any help on this issue would be greatly appreciated. I have done extensive searching on solutions for these problems and have yet to find and answer.
Thanks!
Here is some code snippets to help. This code is also in the GitHub repo if you want to test it out.
In XAML
<Label x:Name="xEmptyListView"
FontSize="16"
HorizontalOptions="CenterAndExpand"
HorizontalTextAlignment="Center"
VerticalOptions="CenterAndExpand"
Text="{Binding ListViewVisibleText, Mode=TwoWay}"
IsVisible="{Binding IsListViewLabelEmptyVisible, Mode=TwoWay }"/>
<ListView x:Name="ItemsListView"
ItemsSource="{Binding Items}"
VerticalOptions="FillAndExpand"
HasUnevenRows="true"
RefreshCommand="{Binding LoadItemsCommand}"
IsPullToRefreshEnabled="true"
IsRefreshing="{Binding IsBusy, Mode=OneWay}"
CachingStrategy="RecycleElement"
ItemSelected="OnItemSelected"
IsVisible="{Binding IsListViewVisible, Mode=TwoWay}"
IsEnabled="{Binding IsActivityRunning, Mode=TwoWay, Converter={StaticResource InverseBooleanConverter}}">
ViewModel
private bool activityRunning { get; set; }
public bool IsActivityRunning
{
get { return activityRunning; }
set
{
if (activityRunning == value)
return;
activityRunning = value;
OnPropertyChanged("IsActivityRunning");
}
}
private string listViewVisibleText { get; set; }
public string ListViewVisibleText
{
get { return listViewVisibleText; }
set
{
if (listViewVisibleText == value)
return;
listViewVisibleText = value;
OnPropertyChanged("ListViewVisibleText");
}
}
private bool listViewLabelEmptyVisible { get; set; }
public bool IsListViewLabelEmptyVisible
{
get
{
if (Items == null || Items.Count == 0)
{
if (IsBusy)
{
ListViewVisibleText = "Loading...";
}
else
{
ListViewVisibleText = "No Items found";
}
listViewLabelEmptyVisible = true;
}
else
{
ListViewVisibleText = string.Empty;
listViewLabelEmptyVisible = false;
}
OnPropertyChanged("IsListViewLabelEmptyVisible");
return listViewLabelEmptyVisible;
}
}
private bool listViewVisible { get; set; }
public bool IsListViewVisible
{
get
{
if (Items == null || Items.Count == 0)
{
listViewVisible = false;
}
else
{
listViewVisible = true;
}
OnPropertyChanged("IsListViewVisible");
return listViewVisible;
}
}
XAML.cs
protected override void OnAppearing()
{
base.OnAppearing();
viewModel.LoadItemsCommand.Execute(null);
}
I am using notify property changed. It is standard code
This is what my view model inherits for the notifyproperty changed
public class ItemsViewModel : BaseViewModel
which base viewmodel inherits from the observable object
public class BaseViewModel : ObservableObject
When you create a test xamarin project this is the way it looks.
public class ObservableObject : INotifyPropertyChanged
{
/// <summary>
/// Sets the property.
/// </summary>
/// <returns><c>true</c>, if property was set, <c>false</c> otherwise.</returns>
/// <param name="backingStore">Backing store.</param>
/// <param name="value">Value.</param>
/// <param name="propertyName">Property name.</param>
/// <param name="onChanged">On changed.</param>
/// <typeparam name="T">The 1st type parameter.</typeparam>
protected bool SetProperty<T>(
ref T backingStore, T value,
[CallerMemberName]string propertyName = "",
Action onChanged = null)
{
if (EqualityComparer<T>.Default.Equals(backingStore, value))
return false;
backingStore = value;
onChanged?.Invoke();
OnPropertyChanged(propertyName);
return true;
}
/// <summary>
/// Occurs when property changed.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raises the property changed event.
/// </summary>
/// <param name="propertyName">Property name.</param>
protected void OnPropertyChanged([CallerMemberName]string propertyName = "")
{
var changed = PropertyChanged;
if (changed == null)
return;
changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Activity running gets set when items are loading etc.
This all works on android. IOS it doesn't.
Sometimes the navigation behaviours are different from iOS and Android. If OnAppearing is not working, you can try to use MessageCenter https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/messaging-center/ to trigger data loading.
I figured this one out. The way I was handling the bindings with showing the label and hiding the list view and vice versa was interfering with the way that IOS handles requests. I removed the bindings and set the controls directly using a property changed handler on the list view. That fixed the on appearing problems for the tabs, the back button etc.
I am creating a UIActivityIndicatorView in my Controller.ViewDidLoad
UIActivityIndicatorView spinner = new UIActivityIndicatorView();
spinner.StartAnimating();
spinner.Hidden = true;
this.Add(spinner);
Then I am binding it with MVVMCross
var set = this.CreateBindingSet<TipView, TipViewModel>();
set.Bind(spinner).For(v => v.Hidden).To(vm => vm.IsBusy).WithConversion("Inverse");
When the View initially loads the UIActivityIndicatorView is spinning and visible. This is incorrect as the IsBusy property is being explicitly set to False in the ViewModel's Init(). I can see this happening and I can see the Converter invert the value.
I know the binding is properly connected because if I fire a command that updates the IsBusy property the Indicator is shown and hidden as I would expect. It is just the initial state that is incorrect.
The StartAnimating method seems to cause the Hidden flag to be overridden. If I do not call StartAnimating the Indicator hides and shows as expected. Of course that means I have a non animating
Indicator.
I could get a WeakReference to the VM, listen to PropertyChanged and call StartAnimating but that seems a bit rubbish.
Does anyone have any better ideas?
Some options you can do:
Subscribe to PropertyChanged changes and write custom code in the event handler (as you suggest in your question)
Inherit from UIActivityIndicatorView and write a public get;set; property which provides the composite functionality (calling Start and Hidden) in the set handler
public class MyIndicatorView : UIActivityIndicatorView {
// ctors
private bool _superHidden;
public bool SuperHidden {
get { return _supperHidden; }
set { _superHidden = value; if (!value) StartAnimating() else StopAnimating(); Hidden = value; }
}
}
Provide a View public get;set; property and put the composite functionality in that (e.g. set.Bind(this).For(v => v.MyAdvanced)...
private bool _myAdvanced;
public bool MyAdvanced {
get { return myAdvanced; }
set { myAdvanced = value; if (!value) _spinner.StartAnimating() else _spinner.StopAnimating(); _spinner.Hidden = value; }
}
Write a custom binding for Hidden which replaces the default functionality and contains the combined Start and Hidden calls (for more on custom bindings, there's a couple of N+1 tutorials)
After reading #slodge's reply I went down the road of Weak Event Listener and ran the code to Hide and StartAnimating in the View. Having copy and pasted that approach 3 times I realised something had to change so I implemented his 4th suggestion and wrote a Custom Binding. FWIW here is that custom binding
/// <summary>
/// Custom Binding for UIActivityIndicator Hidden.
/// This binding will ensure the indicator animates when shown and stops when hidden
/// </summary>
public class ActivityIndicatorViewHiddenTargetBinding : MvxConvertingTargetBinding
{
/// <summary>
/// Initializes a new instance of the <see cref="ActivityIndicatorViewHiddenTargetBinding"/> class.
/// </summary>
/// <param name="target">The target.</param>
public ActivityIndicatorViewHiddenTargetBinding(UIActivityIndicatorView target)
: base(target)
{
if (target == null)
{
MvxBindingTrace.Trace(
MvxTraceLevel.Error,
"Error - UIActivityIndicatorView is null in ActivityIndicatorViewHiddenTargetBinding");
}
}
/// <summary>
/// Gets the default binding mode.
/// </summary>
/// <value>
/// The default mode.
/// </value>
public override MvxBindingMode DefaultMode
{
get { return MvxBindingMode.OneWay; }
}
/// <summary>
/// Gets the type of the target.
/// </summary>
/// <value>
/// The type of the target.
/// </value>
public override System.Type TargetType
{
get { return typeof(bool); }
}
/// <summary>
/// Gets the view.
/// </summary>
/// <value>
/// The view.
/// </value>
protected UIActivityIndicatorView View
{
get { return Target as UIActivityIndicatorView; }
}
/// <summary>
/// Sets the value.
/// </summary>
/// <param name="target">The target.</param>
/// <param name="value">The value.</param>
protected override void SetValueImpl(object target, object value)
{
var view = (UIActivityIndicatorView)target;
if (view == null)
{
return;
}
view.Hidden = (bool)value;
if (view.Hidden)
{
view.StopAnimating();
}
else
{
view.StartAnimating();
}
}
}
I am associating ribbon to my webpart. I have a need to add more than two webparts in a page.
I do not want to have a separate contextual group/tab for each webpart. Is there a way to check if a specific group/tab exists in the current ribbon on the page?
At this point, when I add more than one webpart to the page, I an getting the following error:
Item has already been added. Key in dictionary: 'Ribbon.MyContextualTabGroup' Key being added: 'Ribbon.MyContextualTabGroup'
Here is my code for your reference:
/// <summary>
/// Gets the web part contextual info.
/// </summary>
public WebPartContextualInfo WebPartContextualInfo
{
get
{
var webPartContextualInfo = new WebPartContextualInfo();
var webPartRibbonContextualGroup = new WebPartRibbonContextualGroup();
var webPartRibbonTab = new WebPartRibbonTab();
webPartRibbonContextualGroup.Id = "Ribbon.MyContextualTabGroup";
webPartRibbonContextualGroup.Command = "MyContextualTab.EnableContextualGroup";
webPartRibbonContextualGroup.VisibilityContext = "MyContextualTab.CustomVisibilityContext";
webPartRibbonTab.Id = "Ribbon.MyTab";
webPartRibbonTab.VisibilityContext = "MyContextualTab.CustomVisibilityContext";
webPartContextualInfo.ContextualGroups.Add(webPartRibbonContextualGroup);
webPartContextualInfo.Tabs.Add(webPartRibbonTab);
webPartContextualInfo.PageComponentId = SPRibbon.GetWebPartPageComponentId(this);
return webPartContextualInfo;
}
}
/// <summary>
/// Adds the contextual tab.
/// </summary>
private void AddContextualTab()
{
SPRibbon spRibbon = SPRibbon.GetCurrent(Page);
if (spRibbon == null) return;
var ribbonExtensions = new XmlDocument();
ribbonExtensions.LoadXml(_contextualTab);
spRibbon.RegisterDataExtension(ribbonExtensions.FirstChild, "Ribbon.ContextualTabs._children");
ribbonExtensions.LoadXml(_contextualTabTemplate);
spRibbon.RegisterDataExtension(ribbonExtensions.FirstChild, "Ribbon.Templates._children");
}
/// <summary>
/// The event handler for the System.Web.UI.Control.PreRender event that occurs immediately before the Web Part is rendered to the Web Part Page it is contained on.
/// </summary>
/// <param name="e">A System.EventArgs that contains the event data.</param>
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
AddContextualTab();
ClientScriptManager clientScriptManager = Page.ClientScript;
clientScriptManager.RegisterClientScriptBlock(GetType(), "MyWebPart", DelayScript);
}
Contextual ribbons cannot be shared between different instances of a web part. Since the ribbon will only be displayed if your web part instance has the "focus" on the page. Therefore several instances of the web part have to create their own contextual group.
To avoid ribbon ID duplication append a web part instance specific part to the ribbon IDs. You could use the web part's ID:
webPartRibbonContextualGroup.Id = "Ribbon.MyContextualTabGroup." + ID;
// ...
webPartRibbonTab.Id = "Ribbon.MyTab." + ID;
// etc.