0

I have on a form a RichTextBox, which, depending on certain criteria, should output text in varying different styles.

To try and accomplish this, I've created a small Class ProgressUpdate that should accept the Text to output, but leave the rest as optional.

My primary question is how can I take the values from Args and assign them to the appropriate property?

Also, I'm not sure if this is the best way of doing this sort of thing, so I'm open to suggestions if anyone knows of a better way. Thanks.

Here is my code so far now updated with the the assistance of @Neolisk -

Imports System.Collections.Generic
Imports System.Drawing
Imports System.Reflection

Public Class ProgressUpdate

    Public Property Text As String = ""
    Public Property FontFamily As String = "Calibri"
    Public Property FontSize As Integer = 9
    Public Property FontStyle As FontStyle = FontStyle.Regular
    Public Property Colour As Color = Color.Black

    '**
    ' Constructor
    '*
    Public Sub New(Optional ByVal Text As String = "",
                   Optional ByVal Args As Dictionary(Of String, Object) = Nothing)

        Me.Text = Text

        '** Set the property values from the Args *'
        Dim Type As Type = Me.GetType()
        For Each PropertyName As String In Args.Keys
            Dim PropertyInfo = Type.GetProperty(PropertyName)
            If PropertyInfo IsNot Nothing Then
                PropertyInfo.SetValue(Me, Args(PropertyName), Nothing)
            End If
        Next

    End Sub

End Class
David Gard
  • 11,225
  • 36
  • 115
  • 227

3 Answers3

1

You are looking for a Dictionary class. A List will gladly store equivalent objects for you. Dictionary uses KeyValuePair under the hood, but you can replace by Key with a simple assignment like this (duplicate Keys are not allowed):

dictionary("FontFamily") = "Calibri"

EDIT: Regarding your now primary question (I see you just changed to use a Dictionary), you need to be using reflection. Here is a good answer on that. Or maybe even this one (syntax is easier on the brain). Actually, the last one is so good I am going to post it here:

Dim type As Type = target.GetType();
For Each propName As String In Defaults.Keys
  Dim prop As PropertyInfo = type.GetProperty(propName);
  prop.SetValue(target, Defaults(propName), Nothing);
Next

Where target is your object, which will receive default values, most likely a control in your case.

Community
  • 1
  • 1
Victor Zakharov
  • 25,801
  • 18
  • 85
  • 151
  • Thank you, I have updated my question slightly now that I am using `Dictionary`, but I'm still unsure as to how I can assign the `Value` of each KvP to the appropriate `Property` (which has the same name as the `Key`. Are you able to help with that? Thanks. – David Gard Jan 14 '14 at 15:49
  • In fact, let me elaborate a little more (although I am now just reading your link...) - I realise I could implicitly declare each property, but in reality there will be more than four, so I'm wondering if there is a way of looping to make the routine more efficient. – David Gard Jan 14 '14 at 15:50
  • @DavidGard: Exactly, this is what reflection is for. – Victor Zakharov Jan 14 '14 at 15:51
  • @DavidGard: I updated the answer with how you can loop through Keys in the dictionary. If you were to hard code everything, it would defeat the purpose of reflection. :) – Victor Zakharov Jan 14 '14 at 15:58
  • Thanks for your updates, I'm working through them now. I'm not sure what is meant be `target` though? My understanding is that the target is a `Property` within this `Class`, but as we're setting the value of a `Property`, shouldn't it already know that? – David Gard Jan 14 '14 at 16:03
  • @DavidGard: `target` is the object you are assigning to, most likely a control in your case. – Victor Zakharov Jan 14 '14 at 16:04
  • Since the properties are already known on ProgressUpdate itself, why is reflection needed? Perhaps you are expecting args to pass keys that are not defined in defaults. – Aaron Palmer Jan 14 '14 at 16:17
  • @AaronPalmer: I think the OP is trying to get away from this approach. – Victor Zakharov Jan 14 '14 at 16:32
  • 1
    @Neolisk, perhaps. If we remove the concept of ProgressUpdate altogether, and apply the default values directly to the RichTextBox or to a .skin, then there could simply be a method to process the args and override the default values. In this method, you would use reflection to handle whatever properties were passed. If you stay with the concept of the ProgressUpdate class, I don't see an issue with hard-coding as the properties to be updated already exist in the class itself. – Aaron Palmer Jan 14 '14 at 16:47
  • @Neolisk - Ok, thanks for the help so far guys. So I've updated my question again, this time scrapping the `Defaults` entirely and instead setting them at Class level. Now all I need is a way of taking the values from the given Args (the `Keys` should match the `Properties`). I've tried implementing your example, but it's not working at present. – David Gard Jan 14 '14 at 16:51
  • @AaronPalmer - In answer to your question, say I have 20 Properties (there will be more than on this example), I don't want to have to hard-code a way of checking if a `Key` for each exists in `Args`. I'd also have to hard-code more if I every add more properties. I hope that makes sense! – David Gard Jan 14 '14 at 16:53
  • @DavidGard: What's wrong with your **current** code? Is it giving you runtime errors? Please elaborate. – Victor Zakharov Jan 14 '14 at 16:54
  • It's not actually giving me an error, just freezing. If I comment out that code it works (but of course ignores any given `Args`). Thanks. – David Gard Jan 14 '14 at 16:56
  • @DavidGard, right. But with your concept of ProgressUpdate, you basically are hard-coding each, as a property in the class itself. Are you going to update the ProgressUpdate class with a new property each time you add more properties? – Aaron Palmer Jan 14 '14 at 16:56
  • @DavidGard: Use debugger to step over your code and find which line causes problems. – Victor Zakharov Jan 14 '14 at 16:59
  • @Neolisk - `For Each PropertyName As String In Args.Keys`. It works first time, but then fails. I use the `ProgressUpdate` class at several points, when I need to update a RichTextBox, and I'm calling it like so - `Me.bw.ReportProgress(0, New ProgressUpdate("Beginning profile restoration...", New Dictionary(Of String, Object) From {{"FontStyle", FontStyle.Bold}}))` – David Gard Jan 14 '14 at 17:04
  • @DavidGard, are you really going to hard-code your 20+ properties in every single call to your ReportProgress? If you ask me, that's where your refactor needs to be. – Aaron Palmer Jan 14 '14 at 17:09
  • @DavidGard, ah, that context would be helpful in the original post. – Aaron Palmer Jan 14 '14 at 17:11
  • @DavidGard: How does ProgressUpdate know which control needs to be updated? I mean you update properties in ProgressUpdate, but what gets it further to propagate into a RichTextBox? I am not sure what you are trying to achieve with your current implementation, because the purpose of reflection is avoid those properties at the top of your ProgressUpdate. So please elaborate on that, and also publish relevant code, which shows a call to the above method (constructor in this case). – Victor Zakharov Jan 14 '14 at 19:32
  • @Neolist - ProgressUpdate does not actually update a control itself. When created it is passed to another function that updates the control. This way, every time the control is updated the relevant properties are set. So I'm not trying to avoid setting properties in `ProgressUpdate`, but rather looking for a way of avoiding setting the implicitly (I.e. 20x lines of `Me.Whatever = Args.Whatever`). – David Gard Jan 15 '14 at 09:17
  • @Neolisk - I'm going to post an answer that I now have working, together with an explanation of how I got there. Hopefully we'll both be on the same page at that point! – David Gard Jan 15 '14 at 13:01
1

In light of comments and better understanding the context, I now propose the following:

Public Class ProgressUpdate

    Public Property Text As String = ""
    Public Property FontFamily As String = "Calibri"
    Public Property FontSize As Integer = 9
    Public Property FontStyle As FontStyle = FontStyle.Regular
    Public Property Colour As Color = Color.Black

    Public Sub New(ByVal text As String)
        Me.Text = text
    End Sub

End Class

Used like this:

Dim progressUpdate as New ProgressUpdate("Beginning profile restoration...")
progressUpdate.FontStyle = FontStyle.Bold
' set other properties similarly
Me.bw.ReportProgress(0, progressUpdate)
Aaron Palmer
  • 8,912
  • 9
  • 48
  • 77
  • Basically, you're wanting to assign too much logic inside the ProgressUpdate class. It's just a container and should only be responsible for knowing its own default values. It's caller should be responsible for setting properties if it doesn't like the defaults. – Aaron Palmer Jan 14 '14 at 18:07
  • Pretty much it. I've made my final code a little more complex, somewhere in between what I was going for any what you suggested. Thanks for the tips. – David Gard Jan 15 '14 at 13:19
1

After much toiling and testing I've finally come up with a solution that works.

Initially I was looking for a way of simply setting all of the Properties of a class automatically from a passed Object. I would them pass that set of properties, together with a String, to a function that would update a RichTextBox, using the properties to format the text that was to be appended.

However, it became clear that this would not work because many of the Properties of a RichTextBox are read-only (such as Bold).

So I decided to pass my desired settings and update the RichTextBox all in one, with all of the work now being done by the ProgressUpdate Class.

Thanks to @Neolisk and @AaronPalmer for talking things through with me. I reaslise that this question was not 100% clear/accurate, yet you tried to help any way, for which I am grateful.

Here is an example of how I will use the ProgressUpdate Class -

Class aForm

    Private Property ProgressUpdate As ProgressUpdate

    Public Sub New()
        Me.ProgressUpdate = New ProgressUpdate(Me.CurrentProgressTextbox)
    End Sub

    Private Sub SettingsButton_Click(ByVal sender As Object, ByVal e As EventArgs) Handles SettingsButton.Click
        Call Me.ProgressUpdate.Update("Clicked Settings...")
    End Sub

    Private Sub CancelButton_Click(ByVal sender As Object, ByVal e As EventArgs) Handles CancelButton.Click
        Call Me.ProgressUpdate.Update({{"Text", "You cancelled!!!"}, {"FontStyle", FontStyle.Bold}, {"FontSize", 12.0}})
    End Sub

End Class

And here is the Class itself -

Imports System.Collections.Generic
Imports System.Drawing
Imports System.Windows.Forms

Public Class ProgressUpdate

    Private Property TextBox As RichTextBox
    Private Property Args As Dictionary(Of String, Object)
    Private Property Font As Font

    '**
    ' Constructor
    '*
    Public Sub New(ByVal TextBox As RichTextBox)
        Me.Textbox = TextBox
    End Sub

    '**
    ' Update 'TextBox' with the desired text and style
    '
    ' @param RawArgs Object The Args to use when updating the Textbox (can be either String or Array)
    ' @param Append Boolean Whether or not the Append the Text (Text is overwritten if False)
    '*
    Public Sub Update(Optional ByVal RawArgs As Object = Nothing,
                      Optional ByVal Append As Boolean = True)

        '** Set up Args and ensure that they are valid *'
        Call Me.SetArgs(RawArgs)
        If Me.Args Is Nothing Then Exit Sub

        '** Make sure that Text is declared and set the Text property *'
        Dim Value As Object = Nothing
        If Not Me.Args.TryGetValue("Text", Value) Then Exit Sub
        Dim Text As String = CType(Value, String)

        Call Me.SetFont()   ' Set up the Font to use for the text that is being added

        With Me.TextBox

            If Append Then ' Text is being appended
                If .Text <> "" Then Text = Environment.NewLine & Text ' Add a new line before the Text if necessary
                .AppendText(Text)
            Else ' Text is being overwirtten
                .Text = Text
            End If

            .Select(.GetFirstCharIndexOfCurrentLine(), Text.Length) ' Select all of the last line
            .SelectionFont = Me.Font                                ' Set the desired font
            .Select(.GetFirstCharIndexOfCurrentLine(), 0)           ' Select the current line, with a length of 0
            .ScrollToCaret()                                        ' Scroll to the caret (to show the bottom line)

        End With

    End Sub

    '**
    ' Set the Args property
    '
    ' @param required RawArgs Object    The Args to use when updating the Textbox (can be either String or Array)
    '*
    Private Sub SetArgs(ByVal RawArgs As Object)

        If TypeOf RawArgs Is String Then    ' e.UserState is a String, so it's just the Text that should be added to Args
            Me.Args = New Dictionary(Of String, Object) From {{"Text", RawArgs}}

        ElseIf TypeOf RawArgs Is Array Then ' e.UserState is an Array, so add all of the key/value pairs to Args
            Me.Args = New Dictionary(Of String, Object)
            For KeyIndex As Integer = 0 To UBound(RawArgs)
                Me.Args.Add(RawArgs(KeyIndex, 0), RawArgs(KeyIndex, 1))
            Next

        Else : Me.Args = Nothing   ' There were no Args set
        End If

    End Sub

    '**
    ' Set the correct 'Font' for use on the line that is currently being added to the `TextBox`
    '*
    Private Sub SetFont()

        Dim FontFamily As FontFamily = Nothing
        Dim FontSize As Single = 0
        Dim FontStyle As FontStyle = Nothing
        Dim Value As Object

        With Me.TextBox

            Value = Nothing
            If Me.Args.TryGetValue("FontFamily", Value) Then
                If TypeOf Value Is FontFamily Then
                    FontFamily = Value
                Else : FontFamily = .Font.FontFamily
                End If
            Else : FontFamily = .Font.FontFamily
            End If

            Value = Nothing
            If Me.Args.TryGetValue("FontSize", Value) Then
                If IsNumeric(Value) Then
                    FontSize = Value
                Else : FontSize = .Font.Size
                End If
            Else : FontSize = .Font.Size
            End If

            Value = Nothing
            If Me.Args.TryGetValue("FontStyle", Value) Then
                If TypeOf Value Is FontStyle Then
                    FontStyle = Value
                Else : FontStyle = .Font.Style
                End If
            Else : FontStyle = .Font.Style
            End If

        End With

        Me.Font = New Font(FontFamily, FontSize, FontStyle)

    End Sub

End Class
David Gard
  • 11,225
  • 36
  • 115
  • 227
  • Congratulations. +1. Don't forget to accept an answer. Also, you probably don't need Call statements there. See this - [Call Statement (Visual Basic) @ MSDN](http://msdn.microsoft.com/en-us/library/sxz296wz.aspx). Another side note - you'd want to be using Me.Args.TryGetValue to avoid double lookup. Instead of `CType`, it's better to use `DirectCast` (SetFont procedure). – Victor Zakharov Jan 15 '14 at 13:43
  • Thanks, I've changed `CType` to `DirectCast`. Where are are referring to the use of `Me.Args.TryGetValue` though? Do you mean I should be using it in lieu of `Me.Args` all the time? Finally, I use `Call` just out of habit/preference; I find it easier to scan the code and find what I'm looking for if `Call` is used, and I don't think it has any impact on performance. – David Gard Jan 15 '14 at 15:47
  • Ah, nevermind, I figured ouit the `TryGetValue()` stuff. Do that actually allowed me to get rid of casting altogether in the `SetFont` procedure. I have had to use `CType` once though, as `DirectCast` would not be appropriate for ensuring that the `Args("Text")` is a `String` (perhaps an integer will be passed, for example). – David Gard Jan 15 '14 at 16:35
  • CType is kind of loose. Using [Convert class](http://msdn.microsoft.com/en-us/library/system.convert(v=vs.110).aspx) would probably be more appropriate. Another option - if you know you expect an Integer, you can use `Integer.TryParse`, which is quite self-explanatory, and the syntax is similar to `TryGetValue`. You can also make it more readable by reducing the amount of IF...ELSE pairs, see [this code on pastebin](http://pastebin.com/50yJfxRK#). – Victor Zakharov Jan 15 '14 at 20:28
  • 1
    Thanks, noted and implemented. – David Gard Jan 16 '14 at 09:13