Subclassed RichTextBox OnTextChange event doesn't fire when text first set - c#

I have subclassed a RichTextBox in order to add syntax highlighting, and it works fine when the text is changed manually. However the OnTextChanged event does not fire when the Text is first set in code.
The event code I have is
/// <summary>
/// When text changes keywords are searched for and highlighted
/// </summary>
/// <param name="e"></param>
protected override void OnTextChanged(EventArgs e)
{
if (highlighting)
return;
int currentSelectionStart = this.SelectionStart;
int currentSelectionLength = this.SelectionLength;
base.OnTextChanged(e);
String text = this.Text;
this.Text = "";
this.HighlightSyntax(text);
this.SelectionStart = currentSelectionStart;
this.SelectionLength = currentSelectionLength;
}
How can I get this event to fire when text is set from code e.g. this.structureInFileTextBox.Text = obj.FileStructure;? I've tried overriding the Text property but that makes Visual Studio crash and I have to edit it out of the .cs file before I can open the project again!

I would try this (I only changed this.Text = ""; in base.Text = "";) :
/// <summary>
/// When text changes keywords are searched for and highlighted
/// </summary>
/// <param name="e"></param>
protected override void OnTextChanged(EventArgs e)
{
if (highlighting)
return;
int currentSelectionStart = this.SelectionStart;
int currentSelectionLength = this.SelectionLength;
base.OnTextChanged(e);
String text = this.Text;
base.Text = "";
this.HighlightSyntax(text);
this.SelectionStart = currentSelectionStart;
this.SelectionLength = currentSelectionLength;
}
and override Text property this way :
public new string Text
{
get { return base.Text; }
set
{
if (base.Text != value)
{
base.Text = value;
OnTextChanged(EventArgs.Empty);
}
}
}

Related

How to create a Editable Spinner (Picker)?

I want to create a editable Spinner (or Picker in Xamarin.Forms).
I have a custom renderer for my element (derived from Picker) that render the Picker as AutoCompleteTextView. Inside the renderer i have created AutoCompleteTextView that shows the dropdown menue if it on focus or is been clicked. Its worked fine.
My problem is that it shows like a EditText (or Entry in Xamarin.Forms) control on device, but i want to display it like a Spinner (or Picker on Xamarin.Forms).
Any idea how i have to make this?
EDIT:
Here what i do in UWP:
Custom Renderer for UWP control:
CustomEditablePicker customControl; // Derived from Xamarin.Forms.Picker
ComboBox nativeControl; // Windows.UI.Xaml.Controls.ComboBox
TextBox editControl;
protected override void OnElementChanged(ElementChangedEventArgs<CustomEditablePicker> e)
{
base.OnElementChanged(e);
customControl = e.NewElement;
nativeControl = new ComboBox();
editControl = new TextBox(); // First element of CheckBox would be a TextBox for edit some Text
// Set the style (declarated in App.xaml)
Style editableStyle = App.Current.Resources["ComboBoxItemTextBox"] as Style;
if (editableStyle != null)
{
editControl.Style = editableStyle;
ComboBoxItem item = new ComboBoxItem();
item.IsSelected = true; // Select First element
item.Content = editControl; // TextBox as content for first element
nativeControl.Items.Add(item);
nativeControl.SelectionChanged += NativeControl_SelectionChanged; // Do something if selection is changed
}
// Add items from custom element to native element
foreach (var item in customControl.Items)
{
nativeControl.Items.Add(item);
}
editControl.KeyDown += EditControl_KeyDown; // Handle the space key
editControl.TextChanged += EditControl_TextChanged; // Handle something if text inside TextBox is changed
base.SetNativeControl(nativeControl); // Set native control to be displayed
}
/// <summary>
/// Set text for Picker if value is changed
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void EditControl_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox edit = (sender as TextBox);
customControl.Text = edit.Text;
}
/// <summary>
/// Handle Space-Key, without handle this key the ComboBox would be lost focus
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void EditControl_KeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Key == Windows.System.VirtualKey.Space)
{
if (editControl.SelectionLength > 0)
{
editControl.Text = editControl.Text.Remove(editControl.SelectionStart, editControl.SelectionLength);
editControl.SelectionLength = 0;
}
int pos = editControl.SelectionStart;
editControl.Text = editControl.Text.Insert(pos, " ");
editControl.SelectionStart = pos + 1;
e.Handled = true;
}
}
/// <summary>
/// Occurs when selection of the box is changed
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void NativeControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count == 1 && e.AddedItems[0] != (sender as ComboBox).Items[0])
{
(sender as ComboBox).SelectedIndex = 0;
editControl.Text = e.AddedItems[0] as String;
}
}
And The control in the PCL (Xamarin.Forms):
public class CustomEditablePicker : Picker
{
public static readonly BindableProperty EditTextProperty = BindableProperty.Create<CustomEditablePicker, string>(c => c.Text, String.Empty, BindingMode.TwoWay, propertyChanged: OnTextChanged);
public event EventHandler<CustomUIEventArgs<string>> TextChanged;
public static readonly BindableProperty Source = BindableProperty.Create<CustomEditablePicker, IEnumerable<string>>(l => l.ItemsSource, new List<string>(), BindingMode.TwoWay, propertyChanged: OnSourceChanged);
private static void OnSourceChanged(BindableObject bindable, IEnumerable<string> oldValue, IEnumerable<string> newValue)
{
CustomEditablePicker customEditablePicker = (CustomEditablePicker)bindable;
customEditablePicker.ItemsSource = newValue;
}
public event EventHandler<CustomUIEnumerableArgs<IEnumerable<string>>> SourceChanged;
public IEnumerable<string> ItemsSource
{
get { return (List<string>)this.GetValue(Source); }
set
{
if (this.ItemsSource != value)
{
this.SetValue(Source, value);
if (SourceChanged != null)
{
this.SourceChanged.Invoke(this, new CustomUIEnumerableArgs<IEnumerable<string>>(value));
}
}
}
}
public string Text
{
get { return (string)this.GetValue(EditTextProperty); }
set
{
if (this.Text != value)
{
this.SetValue(EditTextProperty, value);
if (TextChanged != null)
{
// Raise a event, with changed text
this.TextChanged.Invoke(this, new CustomUIEventArgs<string>(value));
}
}
}
}
private static void OnTextChanged(BindableObject bindable, string oldValue, string newValue)
{
CustomEditablePicker customEditablePicker = (CustomEditablePicker)bindable;
customEditablePicker.Text = newValue;
}
}
To show image inside EditText, use SetCompoundDrawablesWithIntrinsicBounds:
protected override void OnElementChanged(ElementChangedEventArgs<SoundsPicker> e)
{
if (e.NewElement != null)
{
if (base.Control == null)
{
EditText editText = new EditText(Context)
{
Focusable = false,
Clickable = true,
Tag = this
};
var padding = (int)Context.ToPixels(10);
// that show image on right side
editText.SetCompoundDrawablesWithIntrinsicBounds(0, 0, Resource.Drawable.arrow_down, 0);
editText.CompoundDrawablePadding = padding;
editText.SetOnClickListener(MyPickerPickerListener.Instance);
editText.SetBackgroundDrawable(null);
SetNativeControl(editText);
}
}
base.OnElementChanged(e);
}
Where is Resource.Drawable.arrow_down is your arrow image.
You can use tools like ILSpy or dotPeek to look at code inside Xamarin assembly.

Press enter button doesn't execute the command with Auto-Complete TextBox

I have a Auto-Complete TextBox like this:
public class AutoCompleteTextBox:ComboBox
{
public AutoCompleteTextBox()
{
ResourceDictionary rd = new ResourceDictionary();
rd.Source = new Uri("/"+this.GetType().Assembly.GetName().Name+";component/Styles/MainViewStyle.xaml",UriKind.Relative);
this.Resources = rd;
this.IsTextSearchEnabled = false;
}
/// <summary>
/// Override OnApplyTemplate method
/// Get TextBox control out of Combobox control, and hook up TextChanged event.
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
//get the textbox control in the ComboBox control
TextBox textBox = this.Template.FindName("PART_EditableTextBox", this) as TextBox;
if (textBox != null)
{
//disable Autoword selection of the TextBox
textBox.AutoWordSelection = false;
//handle TextChanged event to dynamically add Combobox items.
textBox.TextChanged += new TextChangedEventHandler(textBox_TextChanged);
}
}
public ObservableCollection<string> SuggestionList
{
get { return (ObservableCollection<string>)GetValue(SuggestionListProperty); }
set { SetValue(SuggestionListProperty, value); }
}
public static readonly DependencyProperty SuggestionListProperty = DependencyProperty.Register("SuggestionList", typeof(ObservableCollection<string>), typeof(AutoCompleteTextBox), new UIPropertyMetadata());
/// <summary>
/// main logic to generate auto suggestion list.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="System.Windows.Controls.TextChangedEventArgs"/>
/// instance containing the event data.</param>
void textBox_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox textBox = sender as TextBox;
textBox.AutoWordSelection = false;
// if the word in the textbox is selected, then don't change item collection
if ((textBox.SelectionStart != 0 || textBox.Text.Length == 0))
{
this.Items.Clear();
//add new filtered items according the current TextBox input
if (!string.IsNullOrEmpty(textBox.Text))
{
foreach (string s in this.SuggestionList)
{
if (s.StartsWith(textBox.Text, StringComparison.InvariantCultureIgnoreCase))
{
string unboldpart = s.Substring(textBox.Text.Length);
string boldpart = s.Substring(0, textBox.Text.Length);
//construct AutoCompleteEntry and add to the ComboBox
AutoCompleteEntry entry = new AutoCompleteEntry(s, boldpart, unboldpart);
this.Items.Add(entry);
}
}
}
}
// open or close dropdown of the ComboBox according to whether there are items in the
// fitlered result.
this.IsDropDownOpen = this.HasItems;
//avoid auto selection
textBox.Focus();
textBox.SelectionStart = textBox.Text.Length;
}
}
/// <summary>
/// Extended ComboBox Item
/// </summary>
public class AutoCompleteEntry : ComboBoxItem
{
private TextBlock tbEntry;
//text of the item
private string text;
/// <summary>
/// Contrutor of AutoCompleteEntry class
/// </summary>
/// <param name="text">All the Text of the item </param>
/// <param name="bold">The already entered part of the Text</param>
/// <param name="unbold">The remained part of the Text</param>
public AutoCompleteEntry(string text, string bold, string unbold)
{
this.text = text;
tbEntry = new TextBlock();
//highlight the current input Text
tbEntry.Inlines.Add(new Run
{
Text = bold,
FontWeight = FontWeights.Bold,
Foreground = new SolidColorBrush(Colors.RoyalBlue)
});
tbEntry.Inlines.Add(new Run { Text = unbold });
this.Content = tbEntry;
}
/// <summary>
/// Gets the text.
/// </summary>
public string Text
{
get { return this.text; }
}
}
And my xaml looks like this:
<local:AutoCompleteTextBox SuggestionList="{Binding Suggestions}"
Text="{Binding Path=Keyword,UpdateSourceTrigger=PropertyChanged}"
x:Name="SearchTextBox"/>
When I press enter button, I want to execute search command, but if the suggestion combobox on the screen, press enter button only close the suggestion combobox, I need to press enter button again to execute the command. Is there a way to close the suggestion combobox and execute the command with pressing enter button one time?
Thanks
You can invoke the search when handling the DropDownClosed event.
<local:AutoCompleteTextBox SuggestionList="{Binding Suggestions}"
Text="{Binding Path=Keyword,UpdateSourceTrigger=PropertyChanged}"
DropDownClosed="OnDropDownClosed"
x:Name="SearchTextBox"/>
And OnDropDownClosed:
private void OnDropDownClosedobject sender, RoutedPropertyChangedEventArgs<bool> e)
{
// search on Keyword
}

Raise an event after ListBox.Items has changed

anyone know how to raise an event on a ListBox when its redrawn. I'm trying to conditionally mask content in one column but the conditional check seems to be done before the listbox has been drawn and so the mask does not work because there is nothing to mask:
/// <summary>
/// Locks or unlocks the quantity textbox based on 100% flour and activates or deactivate weights
/// </summary>
private void activatePieceQuantity()
{
if (isFlour100Percent())
{
((TextBox)NumberOfItemsTextBox as TextBox).IsEnabled = true;
weightsActive(true);
}
else
{
((TextBox)NumberOfItemsTextBox as TextBox).IsEnabled = false;
weightsActive(false);
}
}
/// <summary>
/// Send controls to search with control name and activate or deactivate flag
/// </summary>
/// <param name="activate"></param>
private void weightsActive(bool activate)
{
int locationInList = 0;
foreach (RecipieIngredient ri in activeRecipie.RecipieIngredients)
{
SearchTree(this.IngredientsListBox.ItemContainerGenerator.ContainerFromIndex(locationInList), "QuanityWeight", activate);
locationInList++;
}
}
/// <summary>
/// Find all weight related objects in the ingredients list and set the visibility accordingly
/// </summary>
/// <param name="targetElement"></param>
/// <param name="flagName">Derived from the Tag of the textbox</param>
/// <param name="enableFlag"></param>
private void SearchTree(DependencyObject targetElement, string flagName, bool enableFlag)
{
if (targetElement == null)
return;
var count = VisualTreeHelper.GetChildrenCount(targetElement);
if (count == 0)
return;
for (int i = 0; i < count; i++)
{
var child = VisualTreeHelper.GetChild(targetElement, i);
if (child is TextBlock)
{
TextBlock targetItem = (TextBlock)child;
if (targetItem.Name == flagName)
if (enableFlag)
{
((TextBlock)targetItem as TextBlock).Visibility = Visibility.Visible;
return;
}
else
{
((TextBlock)targetItem as TextBlock).Visibility = Visibility.Collapsed;
}
}
else
{
SearchTree(child, flagName, enableFlag);
}
}
}
I got it now, the problem was that the ListBox was not drawn when the SearchTree function was called so there was never any DependencyObject to pass to it.
I solved the problem (somewhat hackish in my opinion) by placing a flag in the code to say that the check had been done and then calling the masking function from a LayoutUpdated event
private void IngredientsListBox_LayoutUpdated(object sender, EventArgs e)
{
if (ingredientsListLoaded)
{
activatePieceQuantity();
ingredientsListLoaded = false;
}
}

how to change a button into a imagebutton in asp.net c#

alt text http://[url=http://www.freeimagehosting.net/][img]http://www.freeimagehosting.net/uploads/06e679a07d.jpg[/img][/url]
How to change the button into image button... the button in the beginning has "Pick a date" when clicked a calender pops out and the when a date is selected a label at the bottom reading the date comes in and the text on the button changes to disabled... i want to palce a imagebutton having a image icon of the calender and rest of the function will be the same....
the code as follows:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
[assembly: TagPrefix("DatePicker", "EWS")]
namespace EclipseWebSolutions.DatePicker
{
[DefaultProperty("Text")]
[ToolboxData("<{0}:DatePicker runat=server>")]
[DefaultEvent("SelectionChanged")]
[ValidationProperty("TextValue")]
public class DatePicker : WebControl, INamingContainer
{
#region Properties
public TextBox txtDate = new TextBox();
public Calendar calDate = new Calendar();
public Button btnDate = new Button();
public Panel pnlCalendar = new Panel();
private enum ViewStateConstants
{
ValidationGroup,
RegularExpression,
ErrorMessage,
RegExText,
CalendarPosition,
FormatString,
ExpandLabel,
CollapseLabel,
ApplyDefaultStyle,
CausesValidation,
}
/// <summary>
/// Defines the available display modes of this calendar.
/// </summary>
public enum CalendarDisplay
{
DisplayRight,
DisplayBelow
}
/// <summary>
/// Where to display the popup calendar.
/// </summary>
[Category("Behaviour")]
[Localizable(true)]
public CalendarDisplay CalendarPosition
{
get
{
if (ViewState[ViewStateConstants.CalendarPosition.ToString()] == null)
{
ViewState[ViewStateConstants.CalendarPosition.ToString()] = CalendarDisplay.DisplayRight;
}
return (CalendarDisplay)ViewState[ViewStateConstants.CalendarPosition.ToString()];
}
set
{
ViewState[ViewStateConstants.CalendarPosition.ToString()] = value;
}
}
/// <summary>
/// Text version of the control's value, for use by ASP.NET validators.
/// </summary>
public string TextValue
{
get { return txtDate.Text; }
}
/// <summary>
/// Holds the current date value of this control.
/// </summary>
[Category("Behaviour")]
[Localizable(true)]
[Bindable(true, BindingDirection.TwoWay)]
public DateTime DateValue
{
get
{
try
{
if (txtDate.Text == "") return DateTime.MinValue;
DateTime val = DateTime.Parse(txtDate.Text);
return val;
}
catch (ArgumentNullException)
{
return DateTime.MinValue;
}
catch (FormatException)
{
return DateTime.MinValue;
}
}
set
{
if (value == DateTime.MinValue)
{
txtDate.Text = "";
}
else
{
txtDate.Text = value.ToShortDateString();
}
}
}
[Category("Behavior"), Themeable(false), DefaultValue("")]
public virtual string ValidationGroup
{
get
{
if (ViewState[ViewStateConstants.ValidationGroup.ToString()] == null)
{
return string.Empty;
}
else
{
return (string)ViewState[ViewStateConstants.ValidationGroup.ToString()];
}
}
set
{
ViewState[ViewStateConstants.ValidationGroup.ToString()] = value;
}
}
/// <summary>
/// The label of the exand button. Shown when the calendar is hidden.
/// </summary>
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("PickDate")]
[Localizable(true)]
public string ExpandButtonLabel
{
get
{
String s = (String)ViewState[ViewStateConstants.ExpandLabel.ToString()];
return ((s == null) ? "PickDate" : s);
}
set
{
ViewState[ViewStateConstants.ExpandLabel.ToString()] = value;
}
}
/// <summary>
/// The label of the collapse button. Shown when the calendar is visible.
/// </summary>
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("Disabled")]
[Localizable(true)]
public string CollapseButtonLabel
{
get
{
String s = (String)ViewState[ViewStateConstants.CollapseLabel.ToString()];
return ((s == null) ? "Disabled" : s);
}
set
{
ViewState[ViewStateConstants.CollapseLabel.ToString()] = value;
}
}
/// <summary>
/// Whether to apply the default style. Disable this if you want to apply a custom style, or to use themes and skins
/// to style the control.
/// </summary>
[Category("Appearance")]
[DefaultValue(true)]
[Localizable(true)]
public bool ApplyDefaultStyle
{
get
{
if (ViewState[ViewStateConstants.ApplyDefaultStyle.ToString()] == null)
{
ViewState[ViewStateConstants.ApplyDefaultStyle.ToString()] = true;
}
return (bool)ViewState[ViewStateConstants.ApplyDefaultStyle.ToString()];
}
set
{
ViewState[ViewStateConstants.ApplyDefaultStyle.ToString()] = value;
}
}
/// <summary>
/// Causes Validation
/// </summary>
[Category("Appearance")]
[DefaultValue(false)]
[Localizable(false)]
public bool CausesValidation
{
get
{
if (ViewState[ViewStateConstants.CausesValidation.ToString()] == null)
{
ViewState[ViewStateConstants.CausesValidation.ToString()] = false;
}
return (bool)ViewState[ViewStateConstants.CausesValidation.ToString()];
}
set
{
ViewState[ViewStateConstants.CausesValidation.ToString()] = value;
btnDate.CausesValidation = value;
}
}
#endregion
#region Events
/// <summary>
/// A day was selected from the calendar control.
/// </summary>
public event EventHandler SelectionChanged;
protected virtual void OnSelectionChanged()
{
if (SelectionChanged != null) // only raise the event if someone is listening.
{
SelectionChanged(this, EventArgs.Empty);
}
}
#endregion
#region Event Handlers
/// <summary>
/// The +/- button was clicked.
/// </summary>
protected void btnDate_Click(object sender, System.EventArgs e)
{
if (!calDate.Visible)
{
// expand the calendar
calDate.Visible = true;
txtDate.Enabled = false;
btnDate.Text = CollapseButtonLabel;
if (DateValue != DateTime.MinValue)
{
calDate.SelectedDate = DateValue;
calDate.VisibleDate = DateValue;
}
}
else
{
// collapse the calendar
calDate.Visible = false;
txtDate.Enabled = true;
btnDate.Text = ExpandButtonLabel;
}
}
/// <summary>
/// A date was selected from the calendar.
/// </summary>
protected void calDate_SelectionChanged(object sender, System.EventArgs e)
{
calDate.Visible = false;
txtDate.Visible = true;
btnDate.Text = ExpandButtonLabel;
txtDate.Enabled = true;
txtDate.Text = calDate.SelectedDate.ToShortDateString();
OnSelectionChanged();
}
#endregion
/// <summary>
/// Builds the contents of this control.
/// </summary>
protected override void CreateChildControls()
{
btnDate.Text = ExpandButtonLabel;
btnDate.CausesValidation = CausesValidation;
txtDate.ID = "txtDate";
calDate.Visible = false;
if (ApplyDefaultStyle)
{
calDate.BackColor = System.Drawing.Color.White;
calDate.BorderColor = System.Drawing.Color.FromArgb(10066329);
calDate.CellPadding = 2;
calDate.DayNameFormat = DayNameFormat.Shortest;
calDate.Font.Name = "Verdana";
calDate.Font.Size = FontUnit.Parse("8pt");
calDate.ForeColor = System.Drawing.Color.Black;
calDate.Height = new Unit(150, UnitType.Pixel);
calDate.Width = new Unit(180, UnitType.Pixel);
calDate.DayHeaderStyle.BackColor = System.Drawing.Color.FromArgb(228, 228, 228);
calDate.DayHeaderStyle.Font.Size = FontUnit.Parse("7pt");
calDate.TitleStyle.Font.Bold = true;
calDate.WeekendDayStyle.BackColor = System.Drawing.Color.FromArgb(255, 255, 204);
}
ConnectEventHandlers();
pnlCalendar.Controls.Add(calDate);
pnlCalendar.Style["position"] = "absolute";
pnlCalendar.Style["filter"] = "alpha(opacity=95)";
pnlCalendar.Style["-moz-opacity"] = ".95";
pnlCalendar.Style["opacity"] = ".95";
pnlCalendar.Style["z-index"] = "2";
pnlCalendar.Style["background-color"] = "White";
if (CalendarPosition == CalendarDisplay.DisplayBelow)
{
pnlCalendar.Style["margin-top"] = "27px";
}
else
{
pnlCalendar.Style["display"] = "inline";
}
Controls.Add(txtDate);
Controls.Add(pnlCalendar);
Controls.Add(btnDate);
base.CreateChildControls();
}
/// <summary>
/// Render the contents of this control.
/// </summary>
/// <param name="output">The HtmlTextWriter to use.</param>
protected override void RenderContents(HtmlTextWriter output)
{
switch (CalendarPosition)
{
case CalendarDisplay.DisplayRight:
{
txtDate.RenderControl(output);
btnDate.RenderControl(output);
pnlCalendar.RenderControl(output);
break;
}
case CalendarDisplay.DisplayBelow:
{
pnlCalendar.RenderControl(output);
txtDate.RenderControl(output);
btnDate.RenderControl(output);
break;
}
}
}
/// <summary>
/// Connect event handlers to events.
/// </summary>
private void ConnectEventHandlers()
{
btnDate.Click += new System.EventHandler(btnDate_Click);
calDate.SelectionChanged += new System.EventHandler(calDate_SelectionChanged);
}
}
}
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
[assembly: TagPrefix("DatePicker", "EWS")]
namespace EclipseWebSolutions.DatePicker
{
[DefaultProperty("Text")]
[ToolboxData("<{0}:DatePicker runat=server>")]
[DefaultEvent("SelectionChanged")]
[ValidationProperty("TextValue")]
public class DatePicker : WebControl, INamingContainer
{
#region Properties
public TextBox txtDate = new TextBox();
public Calendar calDate = new Calendar();
public Button btnDate = new Button();
public Panel pnlCalendar = new Panel();
private enum ViewStateConstants
{
ValidationGroup,
RegularExpression,
ErrorMessage,
RegExText,
CalendarPosition,
FormatString,
ExpandLabel,
CollapseLabel,
ApplyDefaultStyle,
CausesValidation,
}
/// <summary>
/// Defines the available display modes of this calendar.
/// </summary>
public enum CalendarDisplay
{
DisplayRight,
DisplayBelow
}
/// <summary>
/// Where to display the popup calendar.
/// </summary>
[Category("Behaviour")]
[Localizable(true)]
public CalendarDisplay CalendarPosition
{
get
{
if (ViewState[ViewStateConstants.CalendarPosition.ToString()] == null)
{
ViewState[ViewStateConstants.CalendarPosition.ToString()] = CalendarDisplay.DisplayRight;
}
return (CalendarDisplay)ViewState[ViewStateConstants.CalendarPosition.ToString()];
}
set
{
ViewState[ViewStateConstants.CalendarPosition.ToString()] = value;
}
}
/// <summary>
/// Text version of the control's value, for use by ASP.NET validators.
/// </summary>
public string TextValue
{
get { return txtDate.Text; }
}
/// <summary>
/// Holds the current date value of this control.
/// </summary>
[Category("Behaviour")]
[Localizable(true)]
[Bindable(true, BindingDirection.TwoWay)]
public DateTime DateValue
{
get
{
try
{
if (txtDate.Text == "") return DateTime.MinValue;
DateTime val = DateTime.Parse(txtDate.Text);
return val;
}
catch (ArgumentNullException)
{
return DateTime.MinValue;
}
catch (FormatException)
{
return DateTime.MinValue;
}
}
set
{
if (value == DateTime.MinValue)
{
txtDate.Text = "";
}
else
{
txtDate.Text = value.ToShortDateString();
}
}
}
[Category("Behavior"), Themeable(false), DefaultValue("")]
public virtual string ValidationGroup
{
get
{
if (ViewState[ViewStateConstants.ValidationGroup.ToString()] == null)
{
return string.Empty;
}
else
{
return (string)ViewState[ViewStateConstants.ValidationGroup.ToString()];
}
}
set
{
ViewState[ViewStateConstants.ValidationGroup.ToString()] = value;
}
}
/// <summary>
/// The label of the exand button. Shown when the calendar is hidden.
/// </summary>
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("PickDate")]
[Localizable(true)]
public string ExpandButtonLabel
{
get
{
String s = (String)ViewState[ViewStateConstants.ExpandLabel.ToString()];
return ((s == null) ? "PickDate" : s);
}
set
{
ViewState[ViewStateConstants.ExpandLabel.ToString()] = value;
}
}
/// <summary>
/// The label of the collapse button. Shown when the calendar is visible.
/// </summary>
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("Disabled")]
[Localizable(true)]
public string CollapseButtonLabel
{
get
{
String s = (String)ViewState[ViewStateConstants.CollapseLabel.ToString()];
return ((s == null) ? "Disabled" : s);
}
set
{
ViewState[ViewStateConstants.CollapseLabel.ToString()] = value;
}
}
/// <summary>
/// Whether to apply the default style. Disable this if you want to apply a custom style, or to use themes and skins
/// to style the control.
/// </summary>
[Category("Appearance")]
[DefaultValue(true)]
[Localizable(true)]
public bool ApplyDefaultStyle
{
get
{
if (ViewState[ViewStateConstants.ApplyDefaultStyle.ToString()] == null)
{
ViewState[ViewStateConstants.ApplyDefaultStyle.ToString()] = true;
}
return (bool)ViewState[ViewStateConstants.ApplyDefaultStyle.ToString()];
}
set
{
ViewState[ViewStateConstants.ApplyDefaultStyle.ToString()] = value;
}
}
/// <summary>
/// Causes Validation
/// </summary>
[Category("Appearance")]
[DefaultValue(false)]
[Localizable(false)]
public bool CausesValidation
{
get
{
if (ViewState[ViewStateConstants.CausesValidation.ToString()] == null)
{
ViewState[ViewStateConstants.CausesValidation.ToString()] = false;
}
return (bool)ViewState[ViewStateConstants.CausesValidation.ToString()];
}
set
{
ViewState[ViewStateConstants.CausesValidation.ToString()] = value;
btnDate.CausesValidation = value;
}
}
#endregion
#region Events
/// <summary>
/// A day was selected from the calendar control.
/// </summary>
public event EventHandler SelectionChanged;
protected virtual void OnSelectionChanged()
{
if (SelectionChanged != null) // only raise the event if someone is listening.
{
SelectionChanged(this, EventArgs.Empty);
}
}
#endregion
#region Event Handlers
/// <summary>
/// The +/- button was clicked.
/// </summary>
protected void btnDate_Click(object sender, System.EventArgs e)
{
if (!calDate.Visible)
{
// expand the calendar
calDate.Visible = true;
txtDate.Enabled = false;
btnDate.Text = CollapseButtonLabel;
if (DateValue != DateTime.MinValue)
{
calDate.SelectedDate = DateValue;
calDate.VisibleDate = DateValue;
}
}
else
{
// collapse the calendar
calDate.Visible = false;
txtDate.Enabled = true;
btnDate.Text = ExpandButtonLabel;
}
}
/// <summary>
/// A date was selected from the calendar.
/// </summary>
protected void calDate_SelectionChanged(object sender, System.EventArgs e)
{
calDate.Visible = false;
txtDate.Visible = true;
btnDate.Text = ExpandButtonLabel;
txtDate.Enabled = true;
txtDate.Text = calDate.SelectedDate.ToShortDateString();
OnSelectionChanged();
}
#endregion
/// <summary>
/// Builds the contents of this control.
/// </summary>
protected override void CreateChildControls()
{
btnDate.Text = ExpandButtonLabel;
btnDate.CausesValidation = CausesValidation;
txtDate.ID = "txtDate";
calDate.Visible = false;
if (ApplyDefaultStyle)
{
calDate.BackColor = System.Drawing.Color.White;
calDate.BorderColor = System.Drawing.Color.FromArgb(10066329);
calDate.CellPadding = 2;
calDate.DayNameFormat = DayNameFormat.Shortest;
calDate.Font.Name = "Verdana";
calDate.Font.Size = FontUnit.Parse("8pt");
calDate.ForeColor = System.Drawing.Color.Black;
calDate.Height = new Unit(150, UnitType.Pixel);
calDate.Width = new Unit(180, UnitType.Pixel);
calDate.DayHeaderStyle.BackColor = System.Drawing.Color.FromArgb(228, 228, 228);
calDate.DayHeaderStyle.Font.Size = FontUnit.Parse("7pt");
calDate.TitleStyle.Font.Bold = true;
calDate.WeekendDayStyle.BackColor = System.Drawing.Color.FromArgb(255, 255, 204);
}
ConnectEventHandlers();
pnlCalendar.Controls.Add(calDate);
pnlCalendar.Style["position"] = "absolute";
pnlCalendar.Style["filter"] = "alpha(opacity=95)";
pnlCalendar.Style["-moz-opacity"] = ".95";
pnlCalendar.Style["opacity"] = ".95";
pnlCalendar.Style["z-index"] = "2";
pnlCalendar.Style["background-color"] = "White";
if (CalendarPosition == CalendarDisplay.DisplayBelow)
{
pnlCalendar.Style["margin-top"] = "27px";
}
else
{
pnlCalendar.Style["display"] = "inline";
}
Controls.Add(txtDate);
Controls.Add(pnlCalendar);
Controls.Add(btnDate);
base.CreateChildControls();
}
/// <summary>
/// Render the contents of this control.
/// </summary>
/// <param name="output">The HtmlTextWriter to use.</param>
protected override void RenderContents(HtmlTextWriter output)
{
switch (CalendarPosition)
{
case CalendarDisplay.DisplayRight:
{
txtDate.RenderControl(output);
btnDate.RenderControl(output);
pnlCalendar.RenderControl(output);
break;
}
case CalendarDisplay.DisplayBelow:
{
pnlCalendar.RenderControl(output);
txtDate.RenderControl(output);
btnDate.RenderControl(output);
break;
}
}
}
/// <summary>
/// Connect event handlers to events.
/// </summary>
private void ConnectEventHandlers()
{
btnDate.Click += new System.EventHandler(btnDate_Click);
calDate.SelectionChanged += new System.EventHandler(calDate_SelectionChanged);
}
}
}
Untitled Page
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
protected void DatePicker1_SelectionChanged(object sender, EventArgs e)
{
Label1.Text = DatePicker1.DateValue.ToShortDateString();
pnlLabel.Update();
}
}
to make the button an image you could just style it to add a background image.
btnDate.style["background-image"] ="url(images/btn.jpg)";
btnDate.text="";
In Html, an image button has a type attribute of 'image' and a src attribute used to determine the image to be displayed. So, to change a standard button to an image button you would:
buttonControl.Attributes["type"] = "image";
buttonControl.Attributes["src"] = "Foo.jpg";
EDIT
If you are trying to change the button type in the click event, then you might be required to do that via javascript client-side. You can do that by registering a start-up script in the Click event like so:
protected void Button_OnClick( object sender, EventArgs e )
{
var button = sender as Button;
if ( button == null )
return;
var jsScript = new StringBuilder();
jsScript.AppendFormat( "<script type=\"text/javascript\">" );
jsScript.AppendFormat( "var button = document.getElementById(\"{0}\"); button.type = \"image\"; button.src=\"Foo.jpg\";"
, button.ClientID);
jsScript.Append( "</script>" );
if ( !Page.ClientScript.IsStartupScriptRegistered( typeof( Page ), "resetImageButton" ) )
Page.ClientScript.RegisterStartupScript( typeof( Page ), "resetImageButton", jsScript.ToString(), false );
}
The catch is that this will only work right after the postback triggered by clicking the button. It will not survive multiple postbacks. For that, you would need to store a flag in a hidden text field or ViewState that indicates which type the button should be and then, based on that, determine whether you need to register this startup script or not.

How do I implement a TextBox that displays "Type here"?

Displaying "Type here to ..." until the user enters text into a TextBox is a well-known usability feature nowadays. How would one implement this feature in C#?
My idea is to override OnTextChanged, but the logic to handle the changes of text from and to "Type here" is a bit tricky...
Displaying "Type here" on initialization and removing it on first input is easy, but I want to display the message every time the entered text becomes empty.
Something that has worked for me:
this.waterMarkActive = true;
this.textBox.ForeColor = Color.Gray;
this.textBox.Text = "Type here";
this.textBox.GotFocus += (source, e) =>
{
if (this.waterMarkActive)
{
this.waterMarkActive = false;
this.textBox.Text = "";
this.textBox.ForeColor = Color.Black;
}
};
this.textBox.LostFocus += (source, e) =>
{
if (!this.waterMarkActive && string.IsNullOrEmpty(this.textBox.Text))
{
this.waterMarkActive = true;
this.textBox.Text = "Type here";
this.textBox.ForeColor = Color.Gray;
}
};
Where bool waterMarkActive is a class member variable and textBox is the TextBox. This probably should be encapsulated though :) There might be some issues with this approach, but I'm not currently aware of any.
I recently discovered that Windows support water marks in text boxes; they are called cue banners (see here). It's very easy to implement:
// Within your class or scoped in a more appropriate location:
[DllImport("user32.dll")]
private static extern IntPtr SendMessage(IntPtr hWnd, int Msg, int wParam, [MarshalAs(UnmanagedType.LPWStr)] string lParam);
// In your constructor or somewhere more suitable:
SendMessage(textBox.Handle, 0x1501, 1, "Please type here.");
Where textBox is an instance of TextBox, 0x1501 is the code for the windows message EM_SETCUEBANNER, the wParam may either be TRUE (non-zero) or FALSE (zero), and lParam is the water mark you'd like to display. wParam indicates when the cue banner should be displayed; if set to TRUE then the cue banner will be displayed even when the control has focus.
What you're looking for is a TextBox with a "watermark".
There's a sample implementation for C# here, all credits to Wael Alghool.
The relevant part of his code is:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Drawing;
namespace wmgCMS
{
class WaterMarkTextBox : TextBox
{
private Font oldFont = null;
private Boolean waterMarkTextEnabled = false;
#region Attributes
private Color _waterMarkColor = Color.Gray;
public Color WaterMarkColor
{
get { return _waterMarkColor; }
set { _waterMarkColor = value; Invalidate();/*thanks to Bernhard Elbl
for Invalidate()*/ }
}
private string _waterMarkText = "Water Mark";
public string WaterMarkText
{
get { return _waterMarkText; }
set { _waterMarkText = value; Invalidate(); }
}
#endregion
//Default constructor
public WaterMarkTextBox()
{
JoinEvents(true);
}
//Override OnCreateControl ... thanks to "lpgray .. codeproject guy"
protected override void OnCreateControl()
{
base.OnCreateControl();
WaterMark_Toggel(null, null);
}
//Override OnPaint
protected override void OnPaint(PaintEventArgs args)
{
// Use the same font that was defined in base class
System.Drawing.Font drawFont = new System.Drawing.Font(Font.FontFamily,
Font.Size, Font.Style, Font.Unit);
//Create new brush with gray color or
SolidBrush drawBrush = new SolidBrush(WaterMarkColor);//use Water mark color
//Draw Text or WaterMark
args.Graphics.DrawString((waterMarkTextEnabled ? WaterMarkText : Text),
drawFont, drawBrush, new PointF(0.0F, 0.0F));
base.OnPaint(args);
}
private void JoinEvents(Boolean join)
{
if (join)
{
this.TextChanged += new System.EventHandler(this.WaterMark_Toggel);
this.LostFocus += new System.EventHandler(this.WaterMark_Toggel);
this.FontChanged += new System.EventHandler(this.WaterMark_FontChanged);
//No one of the above events will start immeddiatlly
//TextBox control still in constructing, so,
//Font object (for example) couldn't be catched from within
//WaterMark_Toggle
//So, call WaterMark_Toggel through OnCreateControl after TextBox
//is totally created
//No doupt, it will be only one time call
//Old solution uses Timer.Tick event to check Create property
}
}
private void WaterMark_Toggel(object sender, EventArgs args )
{
if (this.Text.Length <= 0)
EnableWaterMark();
else
DisbaleWaterMark();
}
private void EnableWaterMark()
{
//Save current font until returning the UserPaint style to false (NOTE:
//It is a try and error advice)
oldFont = new System.Drawing.Font(Font.FontFamily, Font.Size, Font.Style,
Font.Unit);
//Enable OnPaint event handler
this.SetStyle(ControlStyles.UserPaint, true);
this.waterMarkTextEnabled = true;
//Triger OnPaint immediatly
Refresh();
}
private void DisbaleWaterMark()
{
//Disbale OnPaint event handler
this.waterMarkTextEnabled = false;
this.SetStyle(ControlStyles.UserPaint, false);
//Return back oldFont if existed
if(oldFont != null)
this.Font = new System.Drawing.Font(oldFont.FontFamily, oldFont.Size,
oldFont.Style, oldFont.Unit);
}
private void WaterMark_FontChanged(object sender, EventArgs args)
{
if (waterMarkTextEnabled)
{
oldFont = new System.Drawing.Font(Font.FontFamily,Font.Size,Font.Style,
Font.Unit);
Refresh();
}
}
}
}
Based on #Pooven's answer (thank you!), I created this class. Works for me.
/// <summary>
/// A textbox that supports a watermak hint.
/// </summary>
public class WatermarkTextBox : TextBox
{
/// <summary>
/// The text that will be presented as the watermak hint
/// </summary>
private string _watermarkText = "Type here";
/// <summary>
/// Gets or Sets the text that will be presented as the watermak hint
/// </summary>
public string WatermarkText
{
get { return _watermarkText; }
set { _watermarkText = value; }
}
/// <summary>
/// Whether watermark effect is enabled or not
/// </summary>
private bool _watermarkActive = true;
/// <summary>
/// Gets or Sets whether watermark effect is enabled or not
/// </summary>
public bool WatermarkActive
{
get { return _watermarkActive; }
set { _watermarkActive = value; }
}
/// <summary>
/// Create a new TextBox that supports watermak hint
/// </summary>
public WatermarkTextBox()
{
this._watermarkActive = true;
this.Text = _watermarkText;
this.ForeColor = Color.Gray;
GotFocus += (source, e) =>
{
RemoveWatermak();
};
LostFocus += (source, e) =>
{
ApplyWatermark();
};
}
/// <summary>
/// Remove watermark from the textbox
/// </summary>
public void RemoveWatermak()
{
if (this._watermarkActive)
{
this._watermarkActive = false;
this.Text = "";
this.ForeColor = Color.Black;
}
}
/// <summary>
/// Applywatermak immediately
/// </summary>
public void ApplyWatermark()
{
if (!this._watermarkActive && string.IsNullOrEmpty(this.Text)
|| ForeColor == Color.Gray )
{
this._watermarkActive = true;
this.Text = _watermarkText;
this.ForeColor = Color.Gray;
}
}
/// <summary>
/// Apply watermak to the textbox.
/// </summary>
/// <param name="newText">Text to apply</param>
public void ApplyWatermark(string newText)
{
WatermarkText = newText;
ApplyWatermark();
}
}
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern Int32 SendMessage(IntPtr hWnd, int msg, int wParam, [MarshalAs(UnmanagedType.LPWStr)]string lParam);
const int EM_SETCUEBANNER = 0x1501;
public Form1()
{
InitializeComponent();
SendMessage(textBox1.Handle, EM_SETCUEBANNER, 1, "Username");
SendMessage(textBox2.Handle, EM_SETCUEBANNER, 1, "Password");
}
I'm just starting to learn C# this semester so I'm not an expert, but this worked for me:
(This is using windows forms)
private void Form1_Load(object sender, EventArgs e)
{
textBox1.SelectionStart = 0; //This keeps the text
textBox1.SelectionLength = 0; //from being highlighted
textBox1.ForeColor = Color.Gray;
}
private void textBox_MouseMove(object sender, MouseEventArgs e)
{
Cursor.Current = Cursors.IBeam; //Without this the mouse pointer shows busy
}
private void textBox1_KeyDown(object sender, KeyEventArgs e)
{
if (textBox1.Text.Equals("Type here...") == true)
{
textBox1.Text = "";
textBox1.ForeColor = Color.Black;
}
}
private void textBox1_KeyUp(object sender, KeyEventArgs e)
{
if (textBox1.Text.Equals(null) == true || textBox1.Text.Equals("") == true)
{
textBox1.Text = "Type here...";
textBox1.ForeColor = Color.Gray;
}
}
PRODUCES SIMILAR OUTPUT TO HTML WATERMARK
Here is my code for textbox "watermark" or "preview" text - works great! Using Windows Forms Application.
NOTE: This example has 3 text boxes, each has the below method for the "mouse leave" event, and "mouse enter" event respectively.
private void textBoxFav_Leave(object sender, EventArgs e) {
TextBox textbox = (TextBox)sender;
if (String.IsNullOrWhiteSpace(textbox.Text)) {
textbox.ForeColor = Color.Gray;
if (textbox.Name == "textBoxFavFood") {
textbox.Text = "Favorite Food";
}
else if (textbox.Name == "textBoxFavDrink") {
textbox.Text = "Favorite Drink";
}
else if (textbox.Name == "textBoxFavDesert") {
textbox.Text = "Favorite Desert";
}
}
else {
textbox.ForeColor = Color.Black;
}
}
private void textBoxFav_Enter(object sender, EventArgs e) {
TextBox textbox = (TextBox)sender;
if (textbox.Text == "Favorite Food" || textbox.Text == "Favorite Drink" || textbox.Text == "Favorite Desert") {
textbox.Text = "";
textbox.ForeColor = Color.Black;
}
}
Based on answer of Ahmed Soliman Flasha use following class:
public class TextBoxHint : TextBox
{
string _hint;
[Localizable(true)]
public string Hint
{
get { return _hint; }
set { _hint = value; OnHintChanged(); }
}
protected virtual void OnHintChanged()
{
SendMessage(this.Handle, EM_SETCUEBANNER, 1, _hint);
}
const int EM_SETCUEBANNER = 0x1501;
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern Int32 SendMessage(IntPtr hWnd, int msg, int wParam, [MarshalAs(UnmanagedType.LPWStr)]string lParam);
}
Handle the lost focus event and if the property Text is empty, fill it with your default string.
If this is ASP.NET (as opposed to winforms), you could do this:
If you are using jQuery, add this to your document ready (or however you initialize your page):
var $textbox = $("textbox selector"); // assumes you select a single text box
if ($textbox.val() == "") {
$textbox.val("Type here to...");
$textbox.one('focus', function() {
$(this).attr('value', '');
});
}
You'll need to do some small refactoring if you are selecting more than one text box (put the if statement inside of an each on the element).
In the last version of C# the TextBox has the property PlaceholderText, which does all work. So you only need to set "Type here..." as value of this property.
You can draw string "Type here" to the textbox background until it empty
If this is for ASP.NET then you can try TextBoxWatermark.
If this is for Windows Forms, this is already answered here in SO.
Why using OnTextChanged?
I would suggest to remove the text "Type here" when the TextBox gets focus.
When the control loses focus and no text is entered, you can display the text again.
Same result and no need for tricky logic.
If you want to avoid control resizing problems and data binding problems and make the code simpler (ok, it is questionable), you can just use a label and toggle it's visibility. Then
private void FilterComboBox_GotFocus(object sender, EventArgs e)
{
FilterWatermarkLabel.Visible = false;
}
private void FilterComboBox_LostFocus(object sender, EventArgs e)
{
if (!FilterWatermarkLabel.Visible && string.IsNullOrEmpty(FilterComboBox.Text))
{
FilterWatermarkLabel.Visible = true;
}
}
Another approach for images and also avoiding data binding problems is here
https://msdn.microsoft.com/en-us/library/bb613590(v=vs.100).aspx
Based on #Joel's answer. I fix his class (thanks for the base!)
/// <summary>
/// A textbox that supports a watermak hint.
/// Based on: https://stackoverflow.com/a/15232752
/// </summary>
public class WatermarkTextBox : TextBox
{
/// <summary>
/// The text that will be presented as the watermak hint
/// </summary>
private string _watermarkText;
/// <summary>
/// Gets or Sets the text that will be presented as the watermak hint
/// </summary>
public string WatermarkText
{
get { return _watermarkText; }
set { _watermarkText = value; }
}
/// <summary>
/// Whether watermark effect is enabled or not
/// </summary>
private bool _watermarkActive;
/// <summary>
/// Gets or Sets whether watermark effect is enabled or not
/// </summary>
public bool WatermarkActive
{
get { return _watermarkActive; }
set { _watermarkActive = value; }
}
/// <summary>
/// Create a new TextBox that supports watermak hint
/// </summary>
public WatermarkTextBox()
{
this.WatermarkActive = _watermarkActive;
this.Text = _watermarkText;
}
protected override void OnCreateControl()
{
base.OnCreateControl();
if (this.WatermarkActive)
CheckWatermark();
}
protected override void OnGotFocus(EventArgs e)
{
base.OnGotFocus(e);
CheckWatermark();
}
protected override void OnLostFocus(EventArgs e)
{
base.OnLostFocus(e);
CheckWatermark();
}
public void CheckWatermark()
{
if ((this.WatermarkActive) && String.IsNullOrWhiteSpace(this.Text))
{
ForeColor = Color.Gray;
this.Text = _watermarkText;
}
else if ((this.WatermarkActive) && (!String.IsNullOrWhiteSpace(this.Text)))
{
if (this.Text == _watermarkText)
this.Text = "";
ForeColor = Color.Black;
}
else
ForeColor = Color.Black;
}
}
Displaying "Type here to ..." until the user enters text into a TextBox is a well-known usability feature nowadays. How would one implement this feature in C#?
Set textbox.text as "Type here to ..."
create an event, say box_click()
-->Put this code in your method
private void box_Click(object sender, EventArgs e)
{
Textbox b = (Textbox)sender;
b.Text = null;
}
now assign this method to the "Enter" event of your textbox(maybe one or many)

Categories

Resources