I wish I could do the following: #Html.MvcSiteMap().PageMenu() instead of #Html.MvcSiteMap().Menu("MenuBtnGroup").
Mainly because of the filters.
I wish that a certain link to appear only on the page or sitemap and not in the main application menu
<mvcSiteMapNode title="Usuários" controller="Usuarios" action="Index">
<mvcSiteMapNode title="Novo Usuário" action="Novo" visibility="SiteMapPathHelper,PAGEMENU-ONLY,!*" />
<mvcSiteMapNode title="Detalhes" action="Detalhes" visibility="SiteMapPathHelper,!*" dynamicNodeProvider="UsuarioDynamicNodeProvider, Web">
<mvcSiteMapNode title="Editar" action="Editar" inheritedRouteParameters="id" />
</mvcSiteMapNode>
</mvcSiteMapNode>
This functionality isn't built-in (yet), but there is a way to make it happen by building your own visibility provider and using the SourceMetaData to pass the name of the menu into the visibility logic.
/// <summary>
/// Filtered SiteMapNode Visibility Provider for use with named controls.
///
/// Rules are parsed left-to-right, first match wins. Asterisk can be used to match any control or any control name. Exclamation mark can be used to negate a match.
/// </summary>
public class CustomFilteredSiteMapNodeVisibilityProvider
: SiteMapNodeVisibilityProviderBase
{
#region ISiteMapNodeVisibilityProvider Members
/// <summary>
/// Determines whether the node is visible.
/// </summary>
/// <param name="node">The node.</param>
/// <param name="sourceMetadata">The source metadata.</param>
/// <returns>
/// <c>true</c> if the specified node is visible; otherwise, <c>false</c>.
/// </returns>
public override bool IsVisible(ISiteMapNode node, IDictionary<string, object> sourceMetadata)
{
// Is a visibility attribute specified?
string visibility = string.Empty;
if (node.Attributes.ContainsKey("visibility"))
{
visibility = node.Attributes["visibility"].GetType().Equals(typeof(string)) ? node.Attributes["visibility"].ToString() : string.Empty;
}
if (string.IsNullOrEmpty(visibility))
{
return true;
}
visibility = visibility.Trim();
// Check for the source HtmlHelper
if (sourceMetadata["HtmlHelper"] == null)
{
return true;
}
string htmlHelper = sourceMetadata["HtmlHelper"].ToString();
htmlHelper = htmlHelper.Substring(htmlHelper.LastIndexOf(".") + 1);
string name = sourceMetadata["name"].ToString();
// All set. Now parse the visibility variable.
foreach (string visibilityKeyword in visibility.Split(new[] { ',', ';' }))
{
if (visibilityKeyword == htmlHelper || visibilityKeyword == name || visibilityKeyword == "*")
{
return true;
}
else if (visibilityKeyword == "!" + htmlHelper || visibilityKeyword == "!" + name || visibilityKeyword == "!*")
{
return false;
}
}
// Still nothing? Then it's OK!
return true;
}
#endregion
}
Then you can just name each of your menus by giving them a "name" SourceMetadata attribute.
#Html.MvcSiteMap().Menu(new { name = "MainMenu" })
#Html.MvcSiteMap().Menu(new { name = "PageMenu" })
And then use the CustomFilteredSiteMapVisibilityProvider instead of the FilteredVisibilityProvider in your configuration. See this answer for a complete example.
Related
I would to put a svg embedded image as ImageSource for a Button in Xamarin.Forms, something like this
<Button Text="Button" ImageSource="resource://fullname.svg">
</Button>
possibly applying a Transformation to svg (from FFImageLoading.Transformations), but this is a plus.
I've tried this syntax
<Button Text="Button"
ImageSource="{ext:ImageResourceExtension fullname.svg}" />
c#
public class ImageResourceExtension : IMarkupExtension
{
private static Assembly Assembly = typeof(ImageResourceExtension).GetTypeInfo().Assembly;
public string Source { get; set; }
public object ProvideValue(IServiceProvider serviceProvider)
{
if (Source == null) return null;
return SvgImageSource.FromResource(Source, Assembly, 32, 32);
}
}
But it's not working.
Moreover I can't make working this syntax
Source="{resource://fullname.svg, Converter={StaticResource SvgImageSourceConverter}}"
Any help?
Thanks
As Jason said, FFImageLoading support SVG files. Follow the steps below.
Create a Resource folder in your Xamarin.Forms instead of Android part. And then add the SVG file as Embedded resource.
Usage: Use SvgCachedImage to show the embedded svg image and use TapGestureRecognizer to simulate the button click event.
<ffimageloadingsvg:SvgCachedImage
HeightRequest="50"
Source="resource://XamarinDemo.Resources.brightness2.svg"
WidthRequest="50">
<ffimageloadingsvg:SvgCachedImage.GestureRecognizers>
<TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped"></TapGestureRecognizer>
</ffimageloadingsvg:SvgCachedImage.GestureRecognizers>
</ffimageloadingsvg:SvgCachedImage>
Do not forget to add namespace.
xmlns:ffimageloadingsvg="clr-namespace:FFImageLoading.Svg.Forms;assembly=FFImageLoading.Svg.Forms"
Updated: We could use SkiaSharp to draw a image with svg file.
MyControl.cs
public class MyControl : Frame
{
private readonly SKCanvasView _canvasView = new SKCanvasView();
public MyControl()
{
Padding = new Thickness(0);
BackgroundColor = Color.Transparent;
Content = _canvasView;
_canvasView.PaintSurface += CanvasViewOnPaintSurface;
}
public static readonly BindableProperty ImageProperty = BindableProperty.Create(
nameof(Image), typeof(string), typeof(MyControl), default(string), propertyChanged: RedrawCanvas);
public string Image
{
get => (string)GetValue(ImageProperty);
set => SetValue(ImageProperty, value);
}
private static void RedrawCanvas(BindableObject bindable, object oldvalue, object newvalue)
{
MyControl svgIcon = bindable as MyControl;
svgIcon?._canvasView.InvalidateSurface();
}
private void CanvasViewOnPaintSurface(object sender, SKPaintSurfaceEventArgs e)
{
SKCanvas canvas = e.Surface.Canvas;
canvas.Clear();
using (Stream stream = GetType().Assembly.GetManifestResourceStream(Image))
{
SkiaSharp.Extended.Svg.SKSvg svg = new SkiaSharp.Extended.Svg.SKSvg();
svg.Load(stream);
SKImageInfo info = e.Info;
canvas.Translate(info.Width / 2f, info.Height / 2f);
SKRect bounds = svg.ViewBox;
float xRatio = info.Width / bounds.Width;
float yRatio = info.Height / bounds.Height;
float ratio = Math.Min(xRatio, yRatio);
canvas.Scale(ratio);
canvas.Translate(-bounds.MidX, -bounds.MidY);
canvas.DrawPicture(svg.Picture);
}
}
}
Usage:
<local:MyControl
HeightRequest="50"
Image="XamarinDemo.Resources.brightness2.svg"
WidthRequest="50" />
And you could use TapGestureRecognizer to simulate the button click event.
Updated original image resource extension class for Xamarin. OP's class had two issues:
Missing ContentProperty attribute which allows to skip "Source" property name.
Part where partial resource name is converted to a full resource name.
Added optional "Assembly" property, which allows to specify different assembly. In the code, you can also get the full name, resource path, and ImageSource object with the optional replace string map applied, which allows to modify the original SVG content on the fly, e.g. colors).
Usage:
<ff:SvgCachedImage Source="{ImageFromResource file_name.svg}" />
<ff:SvgCachedImage Source="{ImageFromResource Icons/file_name.svg}" />
<Image Source="{ImageFromResource file_name.png}" />
<Image Source="{ImageFromResource file_name.png, Assembly=MyAssembly}" />
ImageFromResourceExtension class:
using FFImageLoading.Svg.Forms;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
/// <summary>
/// Get image source from assembly resource by using partial name.
/// </summary>
/// <example>
/// <ff:SvgCachedImage Source="{ImageFromResource file_name.svg}" />
/// <ff:SvgCachedImage Source="{ImageFromResource Icons/file_name.svg}" />
/// <ff:Image Source="{ImageFromResource file_name.png}" />
/// <ff:Image Source="{ImageFromResource file_name.png, Assembly=MyAssembly}" />
/// </example>
/// <remarks>
/// Parameter format without extension:
/// Source="resource://{AssemblyName}.{PathName}.{FileName}"
/// </remarks>
[ContentProperty(nameof(Source))]
public class ImageFromResourceExtension : IMarkupExtension
{
public static Assembly DefaultAssembly = typeof(ImageResourceExtension).GetTypeInfo().Assembly;
public string Source { get; set; }
public string Assembly { get; set; }
public object ProvideValue(IServiceProvider serviceProvider)
=> GetResourcePath(Source, Assembly);
public static string GetResourcePath(string name, string assembly = null)
=> "resource://" + GetFullName(name, assembly);
public static string GetFullName(string name, string assembly = null)
{
if (string.IsNullOrEmpty(name))
return null;
// Replace folder separators with dots.
name = name.Replace('/', '.');
// Use different assembly if specified.
var asm = string.IsNullOrEmpty(assembly)
? DefaultAssembly
: System.Reflection.Assembly.Load(assembly);
// Find full name of the resource by partial name.
var fullName = asm.GetManifestResourceNames()
.FirstOrDefault(x => x.EndsWith("." + name));
return fullName;
}
public static ImageSource GetImage(string name, string assembly = null, StringDictionaryCollection map = null)
{
var fullName = GetFullName(name);
if (fullName == null)
return null;
// Use different assembly if specified.
var asm = string.IsNullOrEmpty(assembly)
? DefaultAssembly
: System.Reflection.Assembly.Load(assembly);
// Return image source.
return fullName.EndsWith(".svg", StringComparison.OrdinalIgnoreCase)
? SvgImageSource.FromResource(fullName, asm, replaceStringMap: map)
: ImageSource.FromResource(fullName, asm);
}
}
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 have a general need to maintain a reference to my ancestors as I traverse down the sitemap.
Mvc.sitemap
<mvcSiteMap xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-4.0"
xsi:schemaLocation="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-4.0 MvcSiteMapSchema.xsd">
<mvcSiteMapNode title="Home" controller="Home" action="Index" >
<mvcSiteMapNode title="Products" url="~/Home/Products" roles="*">
<mvcSiteMapNode title="Harvest MAX" url="~/Home/Products/HarvestMAX" >
<mvcSiteMapNode title="Policies" url="~/Home/Products/HarvestMAX/Policy/List" productType="HarvestMax" type="P" typeFullName="AACOBusinessModel.AACO.HarvestMax.Policy" roles="*">
<mvcSiteMapNode title="Policy" controller="Object" action="Details" typeName="Policy" typeFullName="AACOBusinessModel.AACO.HarvestMax.Policy" preservedRouteParameters="id" roles="*">
<mvcSiteMapNode title="Counties" controller="Object" action="List" collection="Counties" roles="*">
<mvcSiteMapNode title="County" controller="Object" action="Details" typeName="County" typeFullName="*" preservedRouteParameters="id" roles="*">
<mvcSiteMapNode title="Land Units" controller="Object" action="List" collection="LandUnits" roles="*">
<mvcSiteMapNode title="Land Unit" controller="Object" action="Details" typeName="LandUnit" typeFullName="AACOBusinessModel.AACO.LandUnit" preservedRouteParameters="id" roles="*">
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMap>
Controller
[SiteMapTitle("Label")]
public ActionResult Details(string typeFullName, decimal id)
{
return View(AACOBusinessModel.AACO.VersionedObject.GetObject( typeFullName?.ToType() ?? Startup.CurrentType,
ApplicationSignInManager.UserCredentials.SessionId,
id));
}
There are many reasons I want this, but here are some specific examples.
Example 1: Vanishing ID's
Let's say the url that got me to the Policy node is http://localhost:36695/AACOAgentPortal/details/Policy/814861364767412.
Once I navigate down past that to the County node, my breadcrumbs looks like this:
However if I hover over the Policy breadcrumb, the url given is http://localhost:36695/AACOAgentPortal/Object/Details?typeName=Policy&typeFullName=AACOBusinessModel.AACO.HarvestMax.Policy. As you can see, the id is gone.
Example 2: Vanishing Titles
As you can see in my controller, I'm telling mvc sitemap that I want to use the Label property to display the node title. It does that when it's the leaf node:
But once I go past that, it disappears:
Both of these issues may have a common cause. There are other reasons why I may want a reference to an ancestor along the breadcrumb trail, but these are two concrete ones to exemplify the issue.
I solve this by keeping my objects in a hierarchy in session, and each object has the same key that it's node has so that it can find the node upon processing each request.
MenuItems.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using AtlasKernelBusinessModel;
using MvcSiteMapProvider;
using CommonBusinessModel.Commands;
using CommonBusinessModel.Extensions;
using CommonBusinessModel.Security;
namespace AtlasMvcWebsite.Code
{
[Serializable]
public class MenuItems : Dictionary<string, MenuItem>
{
#region Properties
public IEnumerable<Command> AvailableCommands
{
get
{
return CurrentItem?.Commandable?.AvailableCommands() ?? new List<Command>();
}
}
/// <summary>
/// Each User has his own copy because it has to track his travel through the hierarchy
/// </summary>
public static MenuItems Current
{
get
{
return (MenuItems)(HttpContext.Current.Session["MenuItems"] =
HttpContext.Current.Session["MenuItems"] ??
new MenuItems());
}
}
private MenuItem currentItem;
public MenuItem CurrentItem
{
get
{
return currentItem =
CurrentNode == null ?
null :
this[CurrentNode.Key] =
ContainsKey(CurrentNode.Key) ?
this[CurrentNode.Key] :
new MenuItem(CurrentNode,
CurrentNode.ParentNode != null ? this[CurrentNode.ParentNode.Key] : null);
}
}
public ISiteMapNode CurrentNode { get; private set; }
#endregion
#region Methods
private void Build()
{
Build(SiteMaps.Current.RootNode);
}
private void Build(ISiteMapNode node)
{
foreach (var childNode in node.ChildNodes)
{
foreach (var att in node.Attributes.Where(a => !childNode.Attributes.Any(na => na.Key == a.Key)).ToDictionary(kvp => kvp.Key, kvp => kvp.Value))
{
switch (att.Key)
{
case "productType":
case "typeFullName":
case "typeName":
childNode.Attributes[att.Key] = att.Value;
childNode.RouteValues[att.Key] = att.Value;
break;
}
}
Build(childNode);
}
}
/// <summary>
/// We finally have an object from the details controller and we want to set it to the current menu item
/// </summary>
/// <param name="versionedObject"></param>
/// <returns></returns>
public AtlasKernelBusinessModel.VersionedObject Set(AtlasKernelBusinessModel.VersionedObject versionedObject)
{
((ICommandable)versionedObject).UserAccess = User.Credentials.BusinessObjectAccessFor(versionedObject.ObjectType());
if (CurrentItem != null)
this[CurrentItem.Node.Key].Object = versionedObject;
//else
// for commands
//SiteMapNodeObjects[SiteMapNodeObjects.Last().Key] = versionedObject;
return versionedObject;
}
public void Sync()
{
//Build();
CurrentNode = SiteMaps.Current.CurrentNode;//cache value of current node
Values.ToList().ForEach(m => m.Sync());
}
#endregion
}
}
MenuItem.cs
using AtlasKernelBusinessModel;
using MvcSiteMapProvider;
using System;
using CommonBusinessModel.Commands;
namespace AtlasMvcWebsite.Code
{
[Serializable]
public class MenuItem
{
#region Constructors
public MenuItem(ISiteMapNode node, MenuItem parent)
{
Node = node;
Parent = parent;
}
#endregion
public ISiteMapNode Node;
public readonly MenuItem Parent;
private ICommandable commandable;
public ICommandable Commandable
{
get
{
return commandable;
}
}
private VersionedObject #object;
public VersionedObject Object
{
get
{
return #object;
}
set
{
#object = value;
Type = #object.GetType();
commandable = (ICommandable)#object;
Sync();
}
}
public Type Type;
public void Sync()
{
// sync the node to the object
if (Object == null) return;
Node = SiteMaps.Current.FindSiteMapNodeFromKey(Node.Key);
Node.Title = Type.GetProperty("Label").GetValue(Object).ToString();
Node.RouteValues["id"] = Object.Id;
}
public override string ToString()
{
return $"{Parent?.Node?.Title}.{Node.Title}";
}
}
}
Usage Example
var menuItem = MenuItems.Current.CurrentItem; // ensure current item exists
if (menuItem != null)
{
<!-- CHILD ITEM MENU -->
#Html.MvcSiteMap().Menu(menuItem.Node, true, false, 1)
<!-- COMMAND BUTTONS -->
if (!viewModel.ReadOnly)
{
#Html.DisplayFor(m => menuItem.Commandable.BusinessOperations.Commands)
}
When using preservedRouteParameters, the source of the values it retrieves is from the current request. Therefore, you cannot reuse id for a different purpose if you expect to navigate up the hierarchy. Also, you must ensure that all ancestral preservedRouteParameters are included in the current request or the URLs will not be built correctly.
There is a demo of how to use preservedRouteParameters here: https://github.com/NightOwl888/MvcSiteMapProvider-Remember-User-Position.
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 need to set context language by browser language (languages) and do a redirect to correct language version. I also need to exclude robots due to indexing of search engines.
I found article about "Browser Language Detection is Sitecore" from Mark Stiles, but it wasn't exactly what I wanted because he is overriding native functionality at all.
After several hours I created own (there are parts of Mark's code too) LanguageResolver using for detection of browser language with redirection if is needed and excluding robots.
public class LanguageResolver : Sitecore.Pipelines.HttpRequest.LanguageResolver
{
public override void Process(Sitecore.Pipelines.HttpRequest.HttpRequestArgs args)
{
Assert.ArgumentNotNull(args, "args");
Language lang = GetLanguageFromBrowser(args);
if (lang != null)
{
Sitecore.Context.Language = lang;
// if current language doesn't match with default site language, it will do a redirect
if (Sitecore.Context.Language.Name != Sitecore.Context.Site.Language)
{
// do redirect
UriBuilder uriBuilder = new UriBuilder(args.Context.Request.Url);
uriBuilder.Path = Sitecore.Context.Language.Name + "/";
HttpContext.Current.Response.Redirect(uriBuilder.Uri.ToString(), true);
args.AbortPipeline();
}
}
else
{
base.Process(args);
}
}
/// <summary>
/// Gets language from browser but only when language cookie doesn't exist yet and URL is related to homepage
/// Logic taken from https://markstiles.net/Blog/2013/04/01/browser-language-detection-in-sitecore.aspx
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
public Language GetLanguageFromBrowser(Sitecore.Pipelines.HttpRequest.HttpRequestArgs args)
{
// site must be inicialized, language si resolved only for homepage and a site must have in its configuration resolveBrowserLang="true" !
if (IsBrowserLangDetectionAllowed(args))
{
// default site language
Language defaultLangPrefix = LanguageManager.GetLanguage(Sitecore.Context.Site.Language);
// gets browser languages
string[] userLangs = args.Context.Request.UserLanguages;
if (userLangs != null && userLangs.Any())
{
foreach (string userLang in userLangs)
{
// gets first part where information about language is stored
string lang = userLang.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
if (!String.IsNullOrEmpty(lang))
{
Language scLang;
// if lang starts with the same letters as are in defaultLangPrefix
if (defaultLangPrefix != null && lang.StartsWith(defaultLangPrefix.CultureInfo.TwoLetterISOLanguageName))
{
scLang = defaultLangPrefix;
}
else
{
if (!Language.TryParse(lang, out scLang))
{
continue;
}
}
// then try to get the language item id from the language or two letter iso code
ID scLangId = LanguageManager.GetLanguageItemId(scLang, Sitecore.Context.Database);
if (ID.IsNullOrEmpty(scLangId))
{
//sometimes the language found is slightly different than official language item used in SC
scLang = LanguageManager.GetLanguage(scLang.CultureInfo.TwoLetterISOLanguageName);
scLangId = LanguageManager.GetLanguageItemId(scLang, Sitecore.Context.Database);
}
if (ID.IsNullOrEmpty(scLangId))
{
continue;
}
Item startItem = Sitecore.Context.Database.GetItem(Sitecore.Context.Site.StartPath, scLang);
//if you've got a version in this language then use that language
if (startItem != null && startItem.Versions.Count > 0)
{
return scLang;
}
}
}
// fallback language from site configuration
return LanguageManager.GetLanguage(Sitecore.Context.Site.Properties["resolveBrowserFallbackLang"]);
}
}
return null;
}
/// <summary>
/// Determinates if detection is allowed
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
protected bool IsBrowserLangDetectionAllowed(Sitecore.Pipelines.HttpRequest.HttpRequestArgs args)
{
// site must be defined
return Sitecore.Context.Site != null
// lang cookie is not already set
&& !args.Context.Request.Cookies.AllKeys.Contains(Sitecore.Context.Site.GetCookieKey("lang"))
// URL is related to homepage
&& args.Context.Request.Url.AbsolutePath == "/"
// site must have property resolveBrowserLang="true"
&& Sitecore.Context.Site.Properties["resolveBrowserLang"] == "true"
// user agent is not robot
&& !Sitecore.Analytics.Configuration.AnalyticsSettings.Robots.ExcludeList.ContainsUserAgent(args.Context.Request.UserAgent);
}
}
Client's request was only for homepage, so the script could be extended.