I want to be enter numbers only and does not allow non-numbers, please help me with this.
this Code in TextBox But it does not work with PasswordBox
Dim textBox As TextBox = TryCast(sender, TextBox)
Dim selectionStart As Int32 = textBox.SelectionStart
Dim selectionLength As Int32 = textBox.SelectionLength
Dim newText As [String] = [String].Empty
Dim count As Integer = 0
For Each c As [Char] In textBox.Text.ToCharArray()
If [Char].IsDigit(c) OrElse [Char].IsControl(c) Then
newText += c
End If
Next
textBox.Text = newText
textBox.SelectionStart = If(selectionStart <= textBox.Text.Length, selectionStart, textBox.Text.Length)
thank you
Private Sub PbPhone_PreviewTextInput(sender As Object, e As TextCompositionEventArgs) Handles PbPhone.PreviewTextInput
Dim regex As New Regex("[0-9]+")
If Not regex.IsMatch(e.Text) Then
e.Handled = True
End If
End Sub
Related
Following my previous question answered by #Andrew Morton, I have one more :)
Here is my whole code (not very long for now) :
Imports System.Data
Imports System.Data.SqlClient
Imports System.Data.Sql
Public Class Form1
Sub PopulateCB()
Dim connection As String = "Data Source=.\SQLEXPRESS;Initial Catalog=OST;Integrated Security=True"
Dim sql = "SELECT * FROM liste_unités"
Dim dt As New DataTable
Using conn As New SqlConnection(connection),
da As New SqlDataAdapter(sql, conn)
da.Fill(dt)
End Using
ComboBoxC1L1.DataSource = dt
ComboBoxC1L1.DisplayMember = "nom_unité"
End Sub
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
PopulateCB()
End Sub
Private Sub ComboBoxC1L1_SelectedIndexChanged(sender As Object, e As EventArgs) Handles ComboBoxC1L1.SelectedIndexChanged
Dim cb = DirectCast(sender, ComboBox)
If cb.SelectedIndex >= 0 Then
Dim val = DirectCast(cb.SelectedItem, DataRowView).Row.Field(Of Integer)("cout_unité")
If ComboBoxQC1L1.Text = "ordinaire" Then
LabelPointsC1L1.Text = val
ElseIf ComboBoxQC1L1.Text = "médiocre" Then
LabelPointsC1L1.Text = val - 2
ElseIf ComboBoxQC1L1.Text = "élite" Then
LabelPointsC1L1.Text = val + 2
End If
If cb.SelectedIndex >= 0 Then
Dim val2 = DirectCast(cb.SelectedItem, DataRowView).Row.Field(Of String)("type_unité")
LabelUnitType.Text = val2
End If
End If
Try
Dim totalC1L1 As Integer
totalC1L1 = CInt(TextBoxC1L1.Text) * CInt(LabelPointsC1L1.Text)
LabelTotalC1L1.Text = totalC1L1
Catch ex As Exception
End Try
End Sub
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
ComboBoxQC1L1.Text = "ordinaire"
End Sub
Private Sub TextBoxC1L1_TextChanged(sender As Object, e As EventArgs) Handles TextBoxC1L1.TextChanged
Try
Dim totalC1L1 As Integer
totalC1L1 = CInt(TextBoxC1L1.Text) * CInt(LabelPointsC1L1.Text)
LabelTotalC1L1.Text = totalC1L1
Catch ex As exception
End Try
End Sub
End Class
Here is the program interface
Here is the SQL table look
Here is the program interface when the Button has been clicked
Red Arrow ComboBox text is a DropDownStyle box with 3 possible text choices:
ordinaire,
élite,
médiocre
What I want to do: when changing the red arrow combobox text, the cout_unité label should change too with a "cout_unité -2" in case of "médiocre" ComboBox text, or "cout_unité +2" in case of "élite" ComboBox text or remain = to "cout_unité" if the selected text is "ordinaire".
And it should calculate this only once from the original "cout_unité" value in the table (in case of clicking 10 times on "ordinaire", it shouldn't subtract 10 * 2 to the "cout_unité" value, only 1 * 2)
I can do it in the ComboBoxC1L1 (see code) but I can't reproduce it with this red arrow combobox (probably because of the type of data into this combobox which are "strings", I don't know).
Many thanks :)
Since there is only a single Handles clause, the following line is unnecessary. The sender can only be the ComboBox in the Handles.
Dim cb = DirectCast(sender, ComboBox)
If you set the ValueMember of the combo in the PopulateCB method, you can save a long line of code making the code more readable.
Dim val = DirectCast(cb.SelectedItem, DataRowView).Row.Field(Of Integer)("cout_unité")
To:
Dim val = CInt(ComboBoxC1L1.SelectedValue)
We need the CInt since SelectedValue is an Object.
Don't assign the DataSource until after the DisplayMember and ValueMember are set.
You are checking twice for ComboBoxC1L1.SelectedIndex >= 0.
Just include the unit type in the first If.
The user may not have to trigger the SelectedIndexChanged event if the correct value is already selected. Maybe a button click would be better.
Sub PopulateCB()
Dim connection As String = "Data Source=.\SQLEXPRESS;Initial Catalog=OST;Integrated Security=True"
Dim sql = "SELECT * FROM liste_unités"
Dim dt As New DataTable
Using conn As New SqlConnection(connection),
da As New SqlDataAdapter(sql, conn)
da.Fill(dt)
End Using
ComboBoxC1L1.DisplayMember = "nom_unité"
ComboBoxC1L1.ValueMember = "cout_unité"
ComboBoxC1L1.DataSource = dt
End Sub
Private Sub btnCalculateValue_Click(sender As Object, e As EventArgs) Handles btnCalculateValue.Click
If ComboBoxC1L1.SelectedIndex >= 0 Then
Dim val = CInt(ComboBoxC1L1.SelectedValue)
If ComboBoxQC1L1.Text = "ordinaire" Then
LabelPointsC1L1.Text = val.ToString
ElseIf ComboBoxQC1L1.Text = "médiocre" Then
LabelPointsC1L1.Text = (val - 2).ToString
ElseIf ComboBoxQC1L1.Text = "élite" Then
LabelPointsC1L1.Text = (val + 2).ToString
End If
Dim val2 = DirectCast(ComboBoxC1L1.SelectedItem, DataRowView).Row.Field(Of String)("type_unité")
LabelUnitType.Text = val2
End If
Dim totalC1L1 As Integer
totalC1L1 = CInt(TextBoxC1L1.Text) * CInt(LabelPointsC1L1.Text)
LabelTotalC1L1.Text = totalC1L1.ToString
End Sub
I have a ComboBox, which its ItemsSource is set to an object which inherits ObservableCollection.
The object gets refreshed with new data on a timer.
Since sometimes there is a large set of new data, I don't use the Add method on the ObservableCollection, but rather I use the following code:
For Each itm In MyNewItems
Items.Add(itm)
Next
MyBase.OnPropertyChanged(New PropertyChangedEventArgs("Count"))
OnPropertyChanged(New PropertyChangedEventArgs("Items[]"))
'NEXT LINE CAUSES ISSUE
OnCollectionChanged(New NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))
The problem is that when the last line runs, the Text of the ComboBox gets reset to an empty string.
If I remove that line, then the issue is resolved, but the Items show old data, since the ComboBox doesn't know that new data came in
Please advise
With appreciation
UPDATE
Hi, as requested, I'm posting the relevant code here
1: The Xaml, Pretty Simple:
<Window x:Class="dlgTest"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mch="clr-namespace:Machshevet.Windows;assembly=Machshevet" >
<StackPanel>
<TextBlock Text="{Binding CaseID}"/>
<mch:TestPick Name="cmbTest" SelectedValuePath="ID" DisplayMemberPath="Name" SelectedValue="{Binding CaseID}" IsEditable="True" IsTextSearchEnabled="False" />
</StackPanel>
</Window>
2: The TestPick class, not too complex either:
Public Class TestPick
Inherits ComboBox
Dim usertyped As Boolean
Function Query() As IQueryable
Dim txt = ""
Dispatcher.Invoke(Sub() txt = Text)
Dim ret = GetSlimContext.Query("viwCase").Select("new (ID,Name,ClientName,SubjectName)")
If txt <> "" AndAlso usertyped Then ret = ret.TextFiltered(txt)
Return ret
End Function
Private Sub EntityPick_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
Dim qs = New QuerySource(Function() Query())
Me.ItemsSource = qs
qs.Control = Me
qs.ShouldRefresh = Function() True
End Sub
Private Sub EntityPick_PreviewTextInput(sender As Object, e As TextCompositionEventArgs) Handles Me.PreviewTextInput
usertyped = True
End Sub
Private Sub TestPick_SelectionChanged(sender As Object, e As SelectionChangedEventArgs) Handles Me.SelectionChanged
If e.AddedItems.None Then
Dim a = 1
End If
End Sub
End Class
3: The QuerySource class which does all the heavy lifting
Public Class QuerySource
Inherits ObjectModel.ObservableCollection(Of Object)
Event Refreshed(sender As QuerySource, e As EventArgs)
Property RefreshSpan As TimeSpan = TimeSpan.FromSeconds(3)
Property CheckProperties As Boolean = True
Property Control As ItemsControl
Dim Timer As Threading.Timer = New Threading.Timer(Sub() TimerTick(), Nothing, 0, 600)
Dim _lastRefresh As Date?
Dim Query As Func(Of IQueryable)
Dim workingon As Date?
Sub New(Query As Func(Of IQueryable), Control As ItemsControl)
Me.Control = Control
Me.Query = Query
End Sub
Async Sub TimerTick()
Try
If Now - _lastRefresh.GetValueOrDefault < RefreshSpan Then Exit Sub
If GetLastInputTime() > 60 * 15 Then Exit Sub
Dim isvis = False
Await Control.Dispatcher.BeginInvoke(Sub() isvis = Control.IsUserVisible())
If Not isvis Then Exit Sub
If workingon.HasValue AndAlso workingon.Value > Now.AddSeconds(-15) Then Exit Sub 'if wasnt working for 15 seconds, probaly err or something
workingon = Now
Dim fq = Query.Invoke
Dim itmtype = fq.ElementType
Dim props = itmtype.CachedProperties.Where(Function(x) x.CanWrite AndAlso x.IsScalar(True)).ToList
Dim keyprops = itmtype.CachedKeyProperties.ToList
Dim newData = fq.ToObjectList
If newData Is Nothing Then Exit Sub
Dim keySelector As Func(Of Object, Object)
Dim diff As CollectionDiff(Of Object)
If itmtype.IsScalar Then 'list of strings..
keySelector = Function(x) x
Else
If keyprops.Count <> 1 Then DevError("?")
Dim kp = keyprops.FirstOrDefault
keySelector = Function(x) kp.GetValue(x)
End If
diff = CollectionDiff(Me, newData, keySelector, props, CheckProperties)
Dim toPreserve As Object
ExecIfType(Of Primitives.Selector)(Control, Sub(x) toPreserve = x.Dispatcher.Invoke(Function() x.SelectedItem))
If toPreserve IsNot Nothing Then diff.ToPreserve = {toPreserve}.ToDictionary(Function(x) x, Function(x) Nothing)
diff.PreserveOnDelete = True
If diff.ModificationCount > 400 Or diff.ClearOld Then
CheckReentrancy()
If diff.ClearOld Then
Items.Clear()
Else
For Each pair In diff.ToReplaceByIndex
Control.Dispatcher.Invoke(Sub() Items(pair.Key) = pair.Value)
Next
For Each idx In diff.GetIndexesToDelete
Items.RemoveAt(idx)
Next
End If
For Each itm In diff.ToAdd 'for mem optimization im not using addrange
Items.Add(itm)
Next
MyBase.OnPropertyChanged(New PropertyChangedEventArgs("Count"))
OnPropertyChanged(New PropertyChangedEventArgs("Items[]"))
Control.Dispatcher.Invoke(Sub() OnCollectionChanged(New NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)))
Else
Dim preservIdx = diff.ToPreserve?.Select(Function(x) Items.IndexOf(x.Key))?.ToHashSet
For Each pair In diff.ToReplaceByIndex
Control.Dispatcher.Invoke(Sub() Me(pair.Key) = pair.Value)
Next
For Each idx In diff.GetIndexesToDelete
If diff.PreserveOnDelete AndAlso preservIdx IsNot Nothing AndAlso preservIdx.Contains(idx) Then Continue For
Control.Dispatcher.Invoke(Sub() RemoveAt(idx))
Next
'don't use addrange - will cause a reset
Await Control.Dispatcher.BeginInvoke(Sub() diff.ToAdd.ForEach(Sub(x) Add(x)))
End If
_lastRefresh = Now
workingon = Nothing
Control.Dispatcher.Invoke(Sub()
Dim cvs = System.Windows.Data.CollectionViewSource.GetDefaultView(Me)
If cvs.SortDescriptions.None Then
Dim defsorts = {KVP("Name", False), KVP(NameOf(RecordBase.LastEditedOn), True), KVP(NameOf(LiteRecordBase.ID), True)}
For Each defsort In defsorts
If itmtype.HasProperty(defsort.Key) Then
cvs.SortDescriptions.Add(New SortDescription(defsort.Key, If(defsort.Value, ListSortDirection.Descending, ListSortDirection.Ascending)))
Exit For
End If
Next
End If
End Sub)
RaiseEvent Refreshed(Me, Nothing)
Catch ex As Exception
Control.Dispatcher.BeginInvoke(Sub() ex.Rethrow)
End Try
End Sub
End Class
Okay
Thanks all for chipping in, in the end it seems like my answer is actually here
ObservableCollection : calling OnCollectionChanged with multiple new items
Works like a charm, and thank you all again for your time and patience
I am trying to get all chars of Segoe UI Symbol Font.
I got them, converted to char, converted to Hex value and added to listview as items.
So, somebody else can use their hex values for XAML projects as icon.
But the problem is this in the code:
i am always getting OverFlowException at the function Convert.ToChar.
Code is running correct, but when the index variable is bigger than 65535 which is max char value, i got overflowexception.
But if you run the code, as you will see, in the Segoe UI Symbol fontfamily there are more chars which is bigger than 65535.
Maybe my method is wrong, you can advice me another method.
MainWindow.xaml file:
<Grid Loaded="Grid_Loaded">
<ListView x:Name="listview">
<ListView.View>
<GridView>
<GridViewColumn Header="HexValue" />
</GridView>
</ListView.View>
</ListView>
</Grid>
MainWindow.xaml.vb file
Class MainWindow
Public glyph As GlyphTypeface
Dim characterMap As IDictionary(Of Integer, UShort)
Private Sub Grid_Loaded(sender As Object, e As RoutedEventArgs)
SymbolleriGetir()
End Sub
Public Sub SymbolleriGetir()
Dim segoeUiSymbol As FontFamily
For Each font As FontFamily In Fonts.SystemFontFamilies
Dim fontName As String
fontName = font.Source
If fontName = "Segoe UI Symbol" Then
segoeUiSymbol = font
End If
Next
For Each typeFace As Typeface In segoeUiSymbol.GetTypefaces
typeFace.TryGetGlyphTypeface(glyph)
If glyph IsNot Nothing Then
characterMap = glyph.CharacterToGlyphMap
Else
Continue For
End If
Next
For i As Integer = 0 To characterMap.Keys.Count
Dim index As Integer = characterMap.Keys.ElementAt(i)
Dim c As Char = Nothing
c = Convert.ToChar(index)
Dim charText As String = c.ToString()
listview.Items.Add(String.Format("&#x{0:x2};", System.Convert.ToUInt32(c)))
Next
End Sub
End Class
CharacterToGlyphMap is a lookup map
(IDictionary(Of Integer, UShort))
with the UShort being the unicode char
so it is not necessary to convert.
I am no VB developer, but I just just coded this up and tested which enumerates the chars, and creates an image glyph next to each hex value:
Wingdings:
Your loaded event handler:
(I exited after 100 due to load time)
Private Sub Grid_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
Dim glyph As GlyphTypeface
Dim glyphIndex As UShort
Dim typeface As System.Windows.Media.Typeface = New System.Windows.Media.Typeface("Segoe UI Symbol")
If (typeface.TryGetGlyphTypeface(glyph)) Then
Dim glyphLookupMap As IDictionary(Of Integer, UShort) = glyph.CharacterToGlyphMap
Dim x As Integer = 0
For Each kvp As KeyValuePair(Of Integer, UShort) In glyphLookupMap
Dim c As Char = Convert.ToChar(kvp.Value)
Dim glyphImage As ImageSource = Nothing
If (glyphLookupMap.TryGetValue(kvp.Key, glyphIndex)) Then
glyphImage = Me.CreateGlyph(glyph, glyphIndex, kvp.Value, Brushes.Blue)
End If
Me._listview.Items.Add(Me.CreateGlyphListboxEntry(kvp.Key, glyphImage))
Dim num As Integer = x + 1
x = num
If (num > 100) Then
Exit For
End If
Next
End If
End Sub
And here would be the Glyph image creator
Private Function CreateGlyph(ByVal glyphTypeface As System.Windows.Media.GlyphTypeface, ByVal glyphIndex As UShort, ByVal charUShortVal As UShort, ByVal foreground As Brush) As System.Windows.Media.ImageSource
Dim imageSource As System.Windows.Media.ImageSource
Dim flag As Boolean = False
Dim drawingImage As System.Windows.Media.DrawingImage = Nothing
Try
Dim glyphIndexes As IList(Of UShort) = New List(Of UShort)() From
{
charUShortVal
}
Dim advanceWidths As IList(Of Double) = New List(Of Double)() From
{
glyphTypeface.AdvanceWidths(glyphIndex)
}
Dim glyphRun As System.Windows.Media.GlyphRun = New System.Windows.Media.GlyphRun(glyphTypeface, 0, False, 1, glyphIndexes, New Point(0, 0), advanceWidths, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing)
drawingImage = New System.Windows.Media.DrawingImage(New System.Windows.Media.GlyphRunDrawing(foreground, glyphRun))
Catch exception As System.Exception
imageSource = Nothing
flag = True
End Try
If (Not flag) Then
imageSource = drawingImage
End If
flag = False
Return imageSource
End Function
And finally the Listbox Entry creator:
Private Function CreateGlyphListboxEntry(ByVal charIntValue As Integer, ByVal glyphImage As ImageSource) As FrameworkElement
Dim result As StackPanel = New StackPanel() With
{
.Orientation = Orientation.Horizontal
}
Dim text As TextBlock = New TextBlock() With
{
.Text = String.Format("{0:X}", charIntValue),
.Foreground = Brushes.Black,
.FontSize = 17,
.Margin = New Thickness(10, 0, 10, 0)
}
result.Children.Add(text)
If (glyphImage IsNot Nothing) Then
Dim image As System.Windows.Controls.Image = New System.Windows.Controls.Image()
Dim num As Double = 32
Dim num1 As Double = num
image.Height = num
image.Width = num1
image.Stretch = Stretch.Uniform
image.Source = glyphImage
result.Children.Add(image)
End If
Return result
End Function
Hope this helps!
Ok so I am having problems adding elements into my 2d array. I am using 3 textboxes to allow a user to input items into my array. My problem is I cant seem to get the array to go past (0,2). I want the user to be able to add a row of inputs with each click of the add button. This is so far what I have in my code. Can anyone help? This is not for a class I am learning on my own.
Option Strict On
Option Explicit On
Option Infer Off
Public Class Form1
Private strExams(49, 2) As String
Private Sub btnAdd_Click(sender As Object, e As EventArgs) Handles btnAdd.Click
Dim strStudent As String = txtStudent.Text
Dim strTest As String = txtTest.Text
Dim strScore As String = txtScore.Text
Dim count As Integer = 0
If count <= 49 Then
strExams(count, 0) = strStudent
strExams(count, 1) = strTest
strExams(count, 2) = strScore
count += 1
End If
txtStudent.Text = String.Empty
txtTest.Text = String.Empty
txtScore.Text = String.Empty
txtStudent.Focus()
End Sub
Try this... Your count variable must be placed outside the btnAdd_Click sub or it will always reset back to 0, thus, you won't get past (0,2).
Option Strict On
Option Explicit On
Option Infer Off
Public Class Form1
Private strExams(49, 2) As String
Dim count As Integer = 0
Private Sub btnAdd_Click(sender As Object, e As EventArgs) Handles btnAdd.Click
Dim strStudent As String = txtStudent.Text
Dim strTest As String = txtTest.Text
Dim strScore As String = txtScore.Text
If count <= 49 Then
strExams(count, 0) = strStudent
strExams(count, 1) = strTest
strExams(count, 2) = strScore
count += 1
End If
txtStudent.Text = String.Empty
txtTest.Text = String.Empty
txtScore.Text = String.Empty
txtStudent.Focus()
End Sub
I am inserting lists into a RichTextBox like this - but how do I get the Caret to move to the first list item?
Private Sub TextEditor_BulletListAdd(sender As Object, e As RoutedEventArgs)
Try
Dim vEditor As RichTextBox = TextEditorGrid.FindName("Controls_TextEditorRTF")
Dim vList As New List()
vList.MarkerStyle = TextMarkerStyle.Disc
Dim vRun As New Run()
Dim vItem As New ListItem(New Paragraph(vRun))
vList.ListItems.Add(vItem)
Dim curCaret = vEditor.CaretPosition
Dim curBlock = vEditor.Document.Blocks.Where(Function(x) x.ContentStart.CompareTo(curCaret) = -1 AndAlso x.ContentEnd.CompareTo(curCaret) = 1).FirstOrDefault()
vEditor.Document.Blocks.InsertAfter(curBlock, vList)
Catch ex As Exception
EmailError(ex)
End Try
End Sub
Private Sub TextEditor_NumberListAdd(sender As Object, e As RoutedEventArgs)
Try
Dim vEditor As RichTextBox = TextEditorGrid.FindName("Controls_TextEditorRTF")
Dim vList As New List()
vList.MarkerStyle = TextMarkerStyle.Decimal
Dim vRun As New Run()
Dim vItem As New ListItem(New Paragraph(vRun))
vList.ListItems.Add(vItem)
Dim curCaret = vEditor.CaretPosition
Dim curBlock = vEditor.Document.Blocks.Where(Function(x) x.ContentStart.CompareTo(curCaret) = -1 AndAlso x.ContentEnd.CompareTo(curCaret) = 1).FirstOrDefault()
vEditor.Document.Blocks.InsertAfter(curBlock, vList)
Catch ex As Exception
EmailError(ex)
End Try
End Sub
The easy part is setting the position of the caret... the tricky part is finding the pointer of the place that you want to set it to (unless that is simply the beginning or end of the document):
RichTextBox rtb = new RichTextBox(flowDoc);
// Get the current caret position.
TextPointer caretPos = rtb.CaretPosition;
// Set the TextPointer to the end of the current document.
caretPos = caretPos.DocumentEnd; // <<< You need to find the correct position here
// Specify the new caret position at the end of the current document.
rtb.CaretPosition = caretPos;
From RichTextBox.CaretPosition Property on MSDN.
Turns out the answer was a lot simpler that I thought :-)
Dim vMove As TextPointer = curCaret.GetNextInsertionPosition(LogicalDirection.Forward)
If Not vMove Is Nothing Then
vEditor.CaretPosition = vMove
End If
Complete
Private Sub TextEditor_BulletListAdd(sender As Object, e As RoutedEventArgs)
Try
Dim vEditor As RichTextBox = TextEditorGrid.FindName("Controls_TextEditorRTF")
Dim vList As New List()
vList.MarkerStyle = TextMarkerStyle.Disc
Dim vRun As New Run()
Dim vItem As New ListItem(New Paragraph(vRun))
vList.ListItems.Add(vItem)
Dim curCaret = vEditor.CaretPosition
Dim curBlock = vEditor.Document.Blocks.Where(Function(x) x.ContentStart.CompareTo(curCaret) = -1 AndAlso x.ContentEnd.CompareTo(curCaret) = 1).FirstOrDefault()
vEditor.Document.Blocks.InsertAfter(curBlock, vList)
Dim vMove As TextPointer = curCaret.GetNextInsertionPosition(LogicalDirection.Forward)
If Not vMove Is Nothing Then
vEditor.CaretPosition = vMove
End If
Catch ex As Exception
EmailError(ex)
End Try
End Sub