Dependency Property stops updating after target property changes due to edit in UI - binding

I have a custom dependency property on my control like so (boilerplate to implement the control left out):
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
"Value",
typeof(String),
typeof(BindingTestControl),
new PropertyMetadata(null));
public static void SetValue(UIElement element, string value)
{
element.SetValue(ValueProperty, value);
}
public static string GetValue(UIElement element)
{
return (string)element.GetValue(ValueProperty);
}
I created a page with code-behind to bind to with relevant xaml like so (with x:Name="root" on the page):
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<local:BindingTestControl Value="{Binding ElementName=root, Path=StringItem}"/>
<Button Width="200" Height="100" Tapped="Button_Tapped" FlowDirection="RightToLeft"/>
</Grid>
With code-behind like so (again, only relevant parts shown):
private string stringItem = "";
public string StringItem
{
get
{
return stringItem;
}
set
{
this.stringItem = value;
OnPropertyChanged("StringItem");
}
}
int i = 0;
private void Button_Tapped(object sender, TappedRoutedEventArgs e)
{
//i++;
this.StringItem = "Test" + i;
}
This works fine the first time, but if I update the value in the Textbox, the binding won't overwrite the new value. If I uncomment the i++; then the binding overwrites every time. I'm assuming this happens this way because the value that is being sent with INotifyPropertyChanged is the same as the previous one despite the value in the Textbox no longer being the same.
Is there a way to force the value through the binding even if it hasn't changed?

You could change your setter to something like -
set
{
this.stringItem = null;
this.stringItem = value;
OnPropertyChanged("StringItem");
}
Which should force the PropertyChanged event to fire as a change in value has occurred.

Related

Dynamically changing the localization of an app using Avalonia and resource-files is not working

Intro
I'm working on an application and I want to be able to change the language when the app is running. For cross-platform compatibility I'm using AvaloniaUI.
I've found a few helpful articles:
Simple localization in WPF
Simple localization in WPF, extended for multiple resource-files
Answer to question on StackOverflow (basically the first link)
The problem
On startup of the app a binding is created (in LocExtensionWithMultipleResxFiles) between my control on the view and string this[string key] ( in TranslationSourceWithMultipleResxFiles). The app correctly loads the translations on startup.
On my View I have a button, the ClickEvent correctly sets TranslationSourceWithMultipleResxFiles.Instance.CurrentCulture but the text in my view doesn't update. I'm not sure where I did something wrong or if I need to change the code somewhere, so any help is appreciated.
My code
Using the above articles I have the following code:
TranslationSourceWithMultipleResxFiles contains a Dictionary for all the ResourceManagers that are used. string this[string key] returns the translated text. CurrentCulture is the property you set to change the Culture.
public class TranslationSourceWithMultipleResxFiles : INotifyPropertyChanged
{
public static TranslationSourceWithMultipleResxFiles Instance { get; } = new TranslationSourceWithMultipleResxFiles();
private readonly Dictionary<string, ResourceManager> resourceManagerDictionary = new Dictionary<string, ResourceManager>();
// key is the baseName + stringName that is binded to, this returns the translated text.
public string this[string key]
{
get
{
var (baseName, stringName) = SplitName(key);
string? translation = null;
if (resourceManagerDictionary.ContainsKey(baseName))
translation = resourceManagerDictionary[baseName].GetString(stringName, currentCulture);
return translation ?? key;
}
}
// the culture TranslationSourceWithMultipleResxFiles uses for translations.
private CultureInfo currentCulture = CultureInfo.InstalledUICulture;
public CultureInfo CurrentCulture
{
get { return currentCulture; }
set
{
if (currentCulture != value)
{
currentCulture = value;
NotifyPropertyChanged(string.Empty); // string.Empty/null indicates that all properties have changed
}
}
}
// WPF bindings register PropertyChanged event if the object supports it and update themselves when it is raised
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public void AddResourceManager(ResourceManager resourceManager)
{
if (!resourceManagerDictionary.ContainsKey(resourceManager.BaseName))
resourceManagerDictionary.Add(resourceManager.BaseName, resourceManager);
}
public static (string baseName, string stringName) SplitName(string name)
{
int idx = name.LastIndexOf('.');
return (name.Substring(0, idx), name.Substring(idx + 1));
}
}
In xaml you set the Translation.ResourceManager per UserContorl/Window etc. This is used so multiple resource files can be used in the application. Each child Control looks to this ResourceManager for their translations.
public class Translation : AvaloniaObject
{
public static readonly AttachedProperty<ResourceManager> ResourceManagerProperty = AvaloniaProperty.RegisterAttached<Translation, AvaloniaObject, ResourceManager>("ResourceManager");
public static ResourceManager GetResourceManager(AvaloniaObject dependencyObject)
{
return (ResourceManager)dependencyObject.GetValue(ResourceManagerProperty);
}
public static void SetResourceManager(AvaloniaObject dependencyObject, ResourceManager value)
{
dependencyObject.SetValue(ResourceManagerProperty, value);
}
}
Creates a Binding between the Control on the view and the correct ResourceManager.
public class LocExtensionWithMultipleResxFiles : MarkupExtension
{
public string StringName { get; } // Key name of the translation in a resource file.
public LocExtensionWithMultipleResxFiles(string stringName)
{
StringName = stringName;
}
// Find out what ResourceManager this control uses
private ResourceManager? GetResourceManager(object control)
{
if (control is AvaloniaObject dependencyObject)
{
object localValue = dependencyObject.GetValue(Translation.ResourceManagerProperty);
if (localValue != AvaloniaProperty.UnsetValue)
{
if (localValue is ResourceManager resourceManager)
{
TranslationSourceWithMultipleResxFiles.Instance.AddResourceManager(resourceManager);
return resourceManager;
}
}
}
return null;
}
// Create a binding between the Control and the translated text in a resource file.
public override object ProvideValue(IServiceProvider serviceProvider)
{
object? targetObject = (serviceProvider as IProvideValueTarget)?.TargetObject;
if (targetObject?.GetType().Name == "SharedDp") // is extension used in a control template?
return targetObject; // required for template re-binding
string baseName = GetResourceManager(targetObject)?.BaseName ?? string.Empty; // if the targetObject has a ResourceManager set, BaseName is set
if (string.IsNullOrEmpty(baseName)) // if the targetobjest doesnt have a RM set, it gets the root elements RM.
{
// rootObject is the root control of the visual tree (the top parent of targetObject)
object? rootObject = (serviceProvider as IRootObjectProvider)?.RootObject;
baseName = GetResourceManager(rootObject)?.BaseName ?? string.Empty;
}
if (string.IsNullOrEmpty(baseName)) // template re-binding
{
if (targetObject is Control frameworkElement)
baseName = GetResourceManager(frameworkElement.TemplatedParent)?.BaseName ?? string.Empty;
}
// create a binding between the Control and the correct resource-file
var binding = new ReflectionBindingExtension
{
Mode = BindingMode.OneWay,
Path = $"[{baseName}.{StringName}]", // This is the ResourceManager.Key
Source = TranslationSourceWithMultipleResxFiles.Instance,
FallbackValue = "Fallback, can't set translation.",
TargetNullValue = StringName,
};
return binding.ProvideValue(serviceProvider);
}
}
My View
<Window <!-- Standard Window xaml -->
xmlns:l="clr-namespace:TestAppForMVVMwithBaseClasses.Localization"
l:Translation.ResourceManager="{x:Static p:Resources.ResourceManager}">
<StackPanel>
<TextBlock Text="{l:LocExtensionWithMultipleResxFiles String1}"/>
<Button Content="Nl" Click="CurrentCultureNl_Click"/>
<Button Content="En" Click="CurrentCultureEn_Click"/>
</StackPanel>
</Window>

dom-if element not hiding when condition is false

In a grid I want to display different elements. Depending on the type of the element I want to use a different TemplateRenderer. One solution to this problem would be to use dom-if elements in the template. if the «if» parameter is false, the element should not get rendered. The problem is, that in my templates all the elements get rendered, even though the debugger shows me that the method, that is responsible for determining the truth value, sometimes returns false.
every template gets rendered twice
Here is my code for the grid:
// these are the two javascript templates
#JsModule("./src/views/parts/card/graphics-card-card.js")
#JsModule("./src/views/parts/card/memory-card.js")
public class PcPartsDomIfGrid extends Grid<AbstractPcPart> {
private static final long serialVersionUID = 7474489703766322949L;
public PcPartsDomIfGrid() {
super();
initColumn();
}
private boolean isMemory(AbstractPcPart pcPart) {
return pcPart.getClass().equals(Memory.class);
}
private boolean isGraphicsCard(AbstractPcPart pcPart) {
return pcPart.getClass().equals(GraphicsCard.class);
}
private void initColumn() {
addColumn(Objects.requireNonNull(CardFactory.getTemplate())
.withProperty("partCard", CardFactory::create)
.withProperty("isMemory", this::isMemory)
.withProperty("isGraphicsCard", this::isGraphicsCard));
}
}
Here is the Code for the Factory:
public class CardFactory {
public static AbstractPcPartCard create(AbstractPcPart pcPart) {
if (pcPart.getClass().equals(GraphicsCard.class)) {
return GraphicsCardCard.create((GraphicsCard) pcPart);
} else if (pcPart.getClass().equals(Memory.class)) {
return MemoryCard.create((Memory) pcPart);
} else {
// different PC parts
return null;
}
}
public static TemplateRenderer<AbstractPcPart> getTemplate() {
String memoryTemplate = MemoryCard.getTemplateString();
String graphicsCardTemplate = GraphicsCardCard.getTemplateString();
String combinedTemplate = memoryTemplate + graphicsCardTemplate;
return TemplateRenderer.of(combinedTemplate);
}
}
Both MemoryCard and GraphicsCardCard are similar, here is the code for MemoryCard:
public class MemoryCard extends AbstractPcPartCard {
Memory pcPart;
public MemoryCard(Memory pcPart) {
this.pcPart = pcPart;
}
public static MemoryCard create(Memory pcPart) {
return new MemoryCard(pcPart);
}
// getters
public static String getTemplateString() {
return "<memory-card is='dom-if' if='[[item.isMemory]]'"
+ " part-card='[[item.partCard]]'>"
+ "</memory-card>";
}
}
The complete code can be found on github. The relevant view is located in the package:
com.example.application.ui.views.partsDomIf
The dom-if attribute must be used on a <template> tag, such as
<template is='dom-if' if='[[item.important]]'>this is shown when the item is <b>important</b></template>
<template is='dom-if' if='[[!item.important]]'>this is shown when the item is <b>NOT important</b></template>
Note that the !property.name negation is the only logical operation available, you can't use e.g. comparators there.
The pattern of using a factory method CardFactory::create also does not make much sense to me. The Grid receives a list of items through the DataProvider (either through setItems or setDataProvider) and the Renderer just processes those items to show them in the UI. You should not create new data objects in the Renderer if you can avoid it.

How can I add WaterMark property to PasswordBox in Winrt?

I need WatermarkPasswordBox control but there is not in Winrt. Maybe we can add Watermark property to PasswordBox. Are there anyone who can do it?
Thanks
The purpose of a watermark is to convey a message behind the control. In the case of this demonstration, the watermark also dissappear after you start typing text, so they are more like a field "hint" telling you what is expected.
To achieve this, we turn to a regular WPF solution provider, the AttachedProperty. AttachedProperties allow you to add extra properties to any control. You can also extend it into an Attachedbehaviour, where you are making the control react to changes to the property.
In this example, we use two attached properties. The first "WaterrmarkProperty" to take the watermark value and initialise the control:
public static string GetWatermark(DependencyObject obj)
{
return (string)obj.GetValue(WatermarkProperty);
}
public static void SetWatermark(DependencyObject obj, string value)
{
obj.SetValue(WatermarkProperty, value);
}
public static readonly DependencyProperty WatermarkProperty =
DependencyProperty.RegisterAttached("Watermark", typeof(string), typeof(TextBoxHelper), new UIPropertyMetadata(null, WatermarkChanged));
The second attached property is to notify whether there is a value in the box, which the template binds to and hides or shows the watermark.
public static bool GetShowWatermark(DependencyObject obj)
{
return (bool)obj.GetValue(ShowWatermarkProperty);
}
public static void SetShowWatermark(DependencyObject obj, bool value)
{
obj.SetValue(ShowWatermarkProperty, value);
}
public static readonly DependencyProperty ShowWatermarkProperty =
DependencyProperty.RegisterAttached("ShowWatermark", typeof(bool), typeof(TextBoxHelper), new UIPropertyMetadata(false));
For the TextBoxHelper, whenever the text is changed, the watermark is shown or hidden as follows:
private static void CheckShowWatermark(TextBox box)
{
box.SetValue(TextBoxHelper.ShowWatermarkProperty, box.Text == string.Empty);
}
This is controlled by the ControlTemplate:
<ControlTemplate x:Key="WatermarkedTextBoxTemplate" TargetType="{x:Type TextBox}">
<Microsoft_Windows_Themes:ListBoxChrome x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" RenderMouseOver="{TemplateBinding IsMouseOver}" RenderFocused="{TemplateBinding IsKeyboardFocusWithin}" SnapsToDevicePixels="true">
<Grid>
<TextBlock Text="{Binding Path=(local:TextBoxHelper.Watermark), RelativeSource={RelativeSource TemplatedParent}}" Opacity=".5" FontWeight="Bold" Visibility="{Binding (local:TextBoxHelper.ShowWatermark), Converter={StaticResource BooleanToVisibilityConverter}, RelativeSource={RelativeSource TemplatedParent}}" />
<ScrollViewer x:Name="PART_ContentHost" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Grid>
</Microsoft_Windows_Themes:ListBoxChrome>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
Source: http://code.msdn.microsoft.com/windowsdesktop/Watermarked-TextBox-and-444ebdec
In Windows 8.0 you can use the WatermarkPasswordBox from WinRT XAML Toolkit, which you can get from here. It gives you a Watermark property to set any UI element (Shape, Image, etc.) as a watermark or WatermarkText property that takes a text and WatermarkTextStyle that takes a TextBlock Style to style the text.
In Windows 8.1 you can use the same or use the new PlaceholderText property.
Feel free to rip out and modify the WatermarkPasswordBox control's code from the library and use it in your app. It's MIT licensed. No credits required. Just take the .cs and .xaml files and include the .xaml resource dictionary in your Themes/Generic.xaml like so:
<ResourceDictionary
Source="ms-appx:///YourControlsLibraryNamefNotInMainApp/RelativeDirectoryPathOfTheFile/WatermarkPasswordBox.xaml" />
UPDATE 1
If you don't want to use 3rd party DLL, add these two methods in PasswordBoxBehavior.cs file.
using System.Reflection;
public static T FindVisualChildByName<T>(this DependencyObject fe, string name) where T : DependencyObject
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentNullException("name");
}
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(fe); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(fe, i);
string a = child.GetValue(FrameworkElement.NameProperty) as string;
if (a == name)
{
return child as T;
}
T t = FindVisualChildByName<T>(child, name);
if (t != null)
{
return t;
}
}
return default(T);
}
public static T FindVisualParent<T>(this DependencyObject fe) where T : DependencyObject
{
for (fe = VisualTreeHelper.GetParent(fe); fe != null; fe = VisualTreeHelper.GetParent(fe))
{
T t = fe as T;
if (t != null)
{
return t;
}
}
return default(T);
}
Here's extensive blog from JulMar
Adding a watermark to a PasswordBox in a Windows Store app
Here’s the code if you’d like to use it yourself.

Why is my WPF binding not working in state change?

I have some items in a WrapPanel. I want to be able to click on an item and have it expand to the full width of the wrap panel. I tried doing this by creating two states, Expanded and Colapsed, in the control that is used for each item. For the Expanded state, I bound the Width of the control to be equal to the ActualWidth of the WrapPanel.
When I didn't get the result I expected, I tried setting Expanded value to a specific number (instead of the the binding). That is working. The items toggle between the two Colapsed and Exapanded widths. I still want to have the Expanded state be equal to the width of the WrapPanel though, not an arbitrary fixed width. I know my binding works because if I just bind the Width property directly (not via visual states), the items in the WrapPanel match its width.
Expanded state with Binding - Doesn't work:
<VisualState x:Name="Expanded">
<Storyboard>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.Width)" Storyboard.TargetName="JobMaster">
<EasingDoubleKeyFrame KeyTime="0">
<EasingDoubleKeyFrame.Value>
<Binding
Path="ActualWidth"
RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type WrapPanel}}" />
</EasingDoubleKeyFrame.Value>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
Expanded State with hard coded value - Works
<VisualState x:Name="Expanded">
<Storyboard>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.Width)" Storyboard.TargetName="JobMaster">
<EasingDoubleKeyFrame KeyTime="0" Value="800" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
Bind the control Width property directly works
<UserControl.Width>
<Binding
Path="ActualWidth"
RelativeSource="{RelativeSource AncestorType={x:Type WrapPanel}}" />
</UserControl.Width>
So why doesn't the Binding in the state work or is there another way to do this?
I was never able to get this working using visual states. Instead I wrote a behavoir.
public class TileExpandColapseBehavoir : Behavior<Control>
{
private ITile _data;
#region Properties
public static readonly DependencyProperty TileControlProperty = DependencyProperty.Register("TileControl", typeof(object), typeof(TileExpandColapseBehavoir), new PropertyMetadata(null));
public static readonly DependencyProperty DefaultWidthProperty = DependencyProperty.Register("DefaultWidth", typeof(Double), typeof(TileExpandColapseBehavoir), new PropertyMetadata(null));
public object TileControl
{
get { return (object)this.GetValue(TileControlProperty); }
set { this.SetValue(TileControlProperty, value); }
}
public double DefaultWidth
{
get { return (double)this.GetValue(DefaultWidthProperty); }
set { this.SetValue(DefaultWidthProperty, value); }
}
#endregion
public TileExpandColapseBehavoir()
{
}
protected override void OnAttached()
{
this.AssociatedObject.PreviewMouseDown +=new MouseButtonEventHandler(AssociatedObject_MouseUp);
}
private void AssociatedObject_MouseUp(object sender, MouseButtonEventArgs e)
{
UIElement child = (UIElement)sender;
WrapPanel parentWrap = FindAncestorUtil.TryFindAcestor<WrapPanel>(child);
if (parentWrap != null && TileControl is UserControl)
{
GetData();
if (_data.IsExpanded == false)
{
Binding newBinding = new Binding();
newBinding.Source = parentWrap;
newBinding.Path = new PropertyPath("ActualWidth");
UserControl thisTile = (UserControl)TileControl;
BindingOperations.SetBinding(thisTile, UserControl.WidthProperty, newBinding);
_data.IsExpanded = true;
}
else
{
UserControl thisTile = (UserControl)TileControl;
BindingOperations.ClearBinding(thisTile, UserControl.WidthProperty);
thisTile.Width = DefaultWidth;
_data.IsExpanded = false;
}
}
}
private void GetData()
{
if (_data == null && AssociatedObject.DataContext is ITile)
{
_data = (ITile)AssociatedObject.DataContext;
}
}
}
Your RelativeSource binding is looking for an ancestor of the animation, not the target of the animation. Try giving your WrapPanel a name and use Element binding instead.
<Binding Path="ActualWidth" ElementName="MyWrapPanel"/>

Sharepoint 2007 - cant find my modifications to web.config in SpWebApplication.WebConfigModifications

I cant seem to find the modifications I made to web.config in my FeatureRecievers Activated event. I try to get the modifications from the SpWebApplication.WebConfigModifications collection in the deactivate event, but this is always empty.... And the strangest thing is that my changes are still reverted after deactivating the feature...
My question is, should I not be able to view all changes made to the web.config files when accessing the SpWebApplication.WebConfigModifications collection in the Deactivating event? How should I go about to remove my changes explicitly?
public class FeatureReciever : SPFeatureReceiver
{
private const string FEATURE_NAME = "HelloWorld";
private class Modification
{
public string Name;
public string XPath;
public string Value;
public SPWebConfigModification.SPWebConfigModificationType ModificationType;
public bool createOnly;
public Modification(string name, string xPath, string value, SPWebConfigModification.SPWebConfigModificationType modificationType, bool createOnly)
{
Name = name;
XPath = xPath;
Value = value;
ModificationType = modificationType;
this.createOnly = createOnly;
}
}
private Modification[] modifications =
{
new Modification("connectionStrings", "configuration", "<connectionStrings/>", SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode, true),
new Modification("add[#name='ConnectionString'][#connectionString='Data Source=serverName;Initial Catalog=DBName;User Id=UserId;Password=Pass']", "configuration/connectionStrings", "<add name='ConnectionString' connectionString='Data Source=serverName;Initial Catalog=DBName;User Id=UserId;Password=Pass'/>", SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode, false)
};
public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
SPSite siteCollection = (properties.Feature.Parent as SPWeb).Site as SPSite;
SPWebApplication webApplication = siteCollection.WebApplication;
siteCollection.RootWeb.Title = "Set from activating code at " + DateTime.Now.ToString();
foreach (Modification entry in modifications)
{
SPWebConfigModification webConfigModification = CreateModification(entry);
webApplication.WebConfigModifications.Add(webConfigModification);
}
webApplication.Farm.Services.GetValue<SPWebService>().ApplyWebConfigModifications();
webApplication.WebService.Update();
}
public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
SPSite siteCollection = (properties.Feature.Parent as SPWeb).Site as SPSite;
SPWebApplication webApplication = siteCollection.WebApplication;
siteCollection.RootWeb.Title = "Set from deactivating code at " + DateTime.Now.ToString();
IList<SPWebConfigModification> modifications = webApplication.WebConfigModifications;
foreach (SPWebConfigModification modification in modifications)
{
if (modification.Owner == FEATURE_NAME)
{
webApplication.WebConfigModifications.Remove(modification);
}
}
webApplication.Farm.Services.GetValue<SPWebService>().ApplyWebConfigModifications();
webApplication.WebService.Update();
}
public override void FeatureInstalled(SPFeatureReceiverProperties properties)
{
}
public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
{
}
private SPWebConfigModification CreateModification(Modification entry)
{
SPWebConfigModification spWebConfigModification = new SPWebConfigModification()
{
Name = entry.Name,
Path = entry.XPath,
Owner = FEATURE_NAME,
Sequence = 0,
Type = entry.ModificationType,
Value = entry.Value
};
return spWebConfigModification;
}
}
Thanks for your time.
/Hans
Finally today I figured out what was wrong with my code (that is why the WebConfigModifications collection was empty when i queryied it in the deactivate event) it seems you must apply the changes in a different manner than I had done.
My original approach to applying the changes involved the following code:
Activate event
webApplication.Farm.Services.GetValue().ApplyWebConfigModifications();
webApplication.WebService.Update();
The "correct" way of doing it is this:
SPWebService.ContentService.ApplyWebConfigModifications();
webApplication.Update();
Although I am still at a loss why my original code did not work.. could someone with more knowlege of the configuration object in Sharepoint enlighten me?

Resources